aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml10
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--Gemfile25
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md6
-rw-r--r--RELEASING_RAILS.rdoc45
-rw-r--r--Rakefile36
-rw-r--r--actionmailer/CHANGELOG.md65
-rw-r--r--actionmailer/README.rdoc8
-rw-r--r--actionmailer/actionmailer.gemspec2
-rw-r--r--actionmailer/lib/action_mailer/base.rb51
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb15
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb23
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb4
-rw-r--r--actionmailer/lib/action_mailer/preview.rb14
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb13
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb8
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb2
-rw-r--r--actionmailer/lib/action_mailer/version.rb12
-rw-r--r--actionmailer/test/abstract_unit.rb30
-rw-r--r--actionmailer/test/asset_host_test.rb17
-rw-r--r--actionmailer/test/base_test.rb301
-rw-r--r--actionmailer/test/delivery_methods_test.rb74
-rw-r--r--actionmailer/test/fixtures/test_helper_mailer/welcome1
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb45
-rw-r--r--actionmailer/test/log_subscriber_test.rb8
-rw-r--r--actionmailer/test/mail_layout_test.rb10
-rw-r--r--actionmailer/test/test_helper_test.rb9
-rw-r--r--actionpack/CHANGELOG.md566
-rw-r--r--actionpack/README.rdoc7
-rw-r--r--actionpack/RUNNING_UNIT_TESTS.rdoc6
-rw-r--r--actionpack/actionpack.gemspec2
-rw-r--r--actionpack/lib/abstract_controller/base.rb46
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb79
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb3
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb6
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb8
-rw-r--r--actionpack/lib/abstract_controller/url_for.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb2
-rw-r--r--actionpack/lib/action_controller/caching.rb2
-rw-r--r--actionpack/lib/action_controller/caching/fragments.rb8
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb51
-rw-r--r--actionpack/lib/action_controller/metal.rb25
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-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/http_authentication.rb40
-rw-r--r--actionpack/lib/action_controller/metal/live.rb119
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb6
-rw-r--r--actionpack/lib/action_controller/metal/rack_delegation.rb2
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb23
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb29
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb39
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb12
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb2
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb44
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb1
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb23
-rw-r--r--actionpack/lib/action_controller/railtie.rb4
-rw-r--r--actionpack/lib/action_controller/test_case.rb199
-rw-r--r--actionpack/lib/action_dispatch.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb56
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb14
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb37
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb34
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb100
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb11
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb165
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb33
-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.rb44
-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/nodes/node.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb112
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y21
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb35
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb25
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb97
-rw-r--r--actionpack/lib/action_dispatch/journey/router/strexp.rb15
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb70
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb160
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/index.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb35
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb35
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb5
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb40
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb44
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb (renamed from actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb)0
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb25
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb182
-rw-r--r--actionpack/lib/action_dispatch/routing.rb3
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb10
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb15
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb780
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb241
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb22
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb380
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb13
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb3
-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.rb27
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.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.rb16
-rw-r--r--actionpack/test/abstract_unit.rb166
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb23
-rw-r--r--actionpack/test/controller/assert_select_test.rb29
-rw-r--r--actionpack/test/controller/caching_test.rb51
-rw-r--r--actionpack/test/controller/content_type_test.rb32
-rw-r--r--actionpack/test/controller/filters_test.rb380
-rw-r--r--actionpack/test/controller/flash_test.rb23
-rw-r--r--actionpack/test/controller/force_ssl_test.rb15
-rw-r--r--actionpack/test/controller/http_basic_authentication_test.rb7
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb47
-rw-r--r--actionpack/test/controller/integration_test.rb40
-rw-r--r--actionpack/test/controller/live_stream_test.rb186
-rw-r--r--actionpack/test/controller/localized_templates_test.rb18
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb2
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb2
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb20
-rw-r--r--actionpack/test/controller/mime/respond_with_test.rb35
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb6
-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_implicit_action_test.rb17
-rw-r--r--actionpack/test/controller/new_base/render_plain_test.rb168
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb44
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb29
-rw-r--r--actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb30
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb19
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb26
-rw-r--r--actionpack/test/controller/redirect_test.rb10
-rw-r--r--actionpack/test/controller/render_json_test.rb2
-rw-r--r--actionpack/test/controller/render_other_test.rb11
-rw-r--r--actionpack/test/controller/render_test.rb6
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb66
-rw-r--r--actionpack/test/controller/required_params_test.rb18
-rw-r--r--actionpack/test/controller/routing_test.rb330
-rw-r--r--actionpack/test/controller/send_file_test.rb6
-rw-r--r--actionpack/test/controller/show_exceptions_test.rb2
-rw-r--r--actionpack/test/controller/test_case_test.rb48
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb81
-rw-r--r--actionpack/test/controller/url_for_test.rb42
-rw-r--r--actionpack/test/dispatch/cookies_test.rb117
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb40
-rw-r--r--actionpack/test/dispatch/header_test.rb2
-rw-r--r--actionpack/test/dispatch/live_response_test.rb15
-rw-r--r--actionpack/test/dispatch/mapper_test.rb8
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb2
-rw-r--r--actionpack/test/dispatch/mount_test.rb24
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb132
-rw-r--r--actionpack/test/dispatch/rack_test.rb191
-rw-r--r--actionpack/test/dispatch/reloader_test.rb5
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb10
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb3
-rw-r--r--actionpack/test/dispatch/request/session_test.rb41
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb5
-rw-r--r--actionpack/test/dispatch/request_test.rb598
-rw-r--r--actionpack/test/dispatch/response_test.rb6
-rw-r--r--actionpack/test/dispatch/routing/concerns_test.rb3
-rw-r--r--actionpack/test/dispatch/routing/route_set_test.rb4
-rw-r--r--actionpack/test/dispatch/routing_test.rb625
-rw-r--r--actionpack/test/dispatch/session/cache_store_test.rb17
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb21
-rw-r--r--actionpack/test/dispatch/show_exceptions_test.rb3
-rw-r--r--actionpack/test/dispatch/ssl_test.rb7
-rw-r--r--actionpack/test/dispatch/template_assertions_test.rb98
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb12
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb34
-rw-r--r--actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb3
-rw-r--r--actionpack/test/fixtures/test/_changing_priority.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_changing_priority.json.erb1
-rw-r--r--actionpack/test/fixtures/test/_counter.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_customer.erb1
-rw-r--r--actionpack/test/fixtures/test/_customer_counter.erb1
-rw-r--r--actionpack/test/fixtures/test/_customer_counter_with_as.erb1
-rw-r--r--actionpack/test/fixtures/test/_customer_greeting.erb1
-rw-r--r--actionpack/test/fixtures/test/_customer_with_var.erb1
-rw-r--r--actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_first_json_partial.json.erb1
-rw-r--r--actionpack/test/fixtures/test/_form.erb1
-rw-r--r--actionpack/test/fixtures/test/_hash_greeting.erb1
-rw-r--r--actionpack/test/fixtures/test/_hash_object.erb2
-rw-r--r--actionpack/test/fixtures/test/_hello.builder1
-rw-r--r--actionpack/test/fixtures/test/_json_change_priority.json.erb0
-rw-r--r--actionpack/test/fixtures/test/_labelling_form.erb1
-rw-r--r--actionpack/test/fixtures/test/_layout_for_partial.html.erb3
-rw-r--r--actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_partial_html_erb.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_partial_name_local_variable.erb1
-rw-r--r--actionpack/test/fixtures/test/_partial_only.erb1
-rw-r--r--actionpack/test/fixtures/test/_partial_only_html.html1
-rw-r--r--actionpack/test/fixtures/test/_partial_with_partial.erb2
-rw-r--r--actionpack/test/fixtures/test/_person.erb2
-rw-r--r--actionpack/test/fixtures/test/_raise_indentation.html.erb13
-rw-r--r--actionpack/test/fixtures/test/_second_json_partial.json.erb1
-rw-r--r--actionpack/test/fixtures/test/action_talk_to_layout.erb2
-rw-r--r--actionpack/test/fixtures/test/calling_partial_with_layout.html.erb1
-rw-r--r--actionpack/test/fixtures/test/capturing.erb4
-rw-r--r--actionpack/test/fixtures/test/change_priority.html.erb2
-rw-r--r--actionpack/test/fixtures/test/content_for.erb1
-rw-r--r--actionpack/test/fixtures/test/content_for_concatenated.erb3
-rw-r--r--actionpack/test/fixtures/test/content_for_with_parameter.erb2
-rw-r--r--actionpack/test/fixtures/test/formatted_html_erb.html.erb1
-rw-r--r--actionpack/test/fixtures/test/greeting.html.erb1
-rw-r--r--actionpack/test/fixtures/test/greeting.xml.erb1
-rw-r--r--actionpack/test/fixtures/test/hello,world.erb1
-rw-r--r--actionpack/test/fixtures/test/hello.builder4
-rw-r--r--actionpack/test/fixtures/test/hello_world_container.builder3
-rw-r--r--actionpack/test/fixtures/test/hello_world_from_rxml.builder3
-rw-r--r--actionpack/test/fixtures/test/hello_world_with_layout_false.erb1
-rw-r--r--actionpack/test/fixtures/test/html_template.html.erb1
-rw-r--r--actionpack/test/fixtures/test/hyphen-ated.erb1
-rw-r--r--actionpack/test/fixtures/test/list.erb1
-rw-r--r--actionpack/test/fixtures/test/non_erb_block_content_for.builder4
-rw-r--r--actionpack/test/fixtures/test/potential_conflicts.erb4
-rw-r--r--actionpack/test/fixtures/test/proper_block_detection.erb1
-rw-r--r--actionpack/test/fixtures/test/render_file_from_template.html.erb1
-rw-r--r--actionpack/test/fixtures/test/render_file_with_locals_and_default.erb1
-rw-r--r--actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb1
-rw-r--r--actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb1
-rw-r--r--actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb1
-rw-r--r--actionpack/test/fixtures/test/render_partial_inside_directory.html.erb1
-rw-r--r--actionpack/test/fixtures/test/render_to_string_test.erb1
-rw-r--r--actionpack/test/fixtures/test/render_two_partials.html.erb2
-rw-r--r--actionpack/test/fixtures/test/using_layout_around_block.html.erb1
-rw-r--r--actionpack/test/fixtures/test/with_html_partial.html.erb1
-rw-r--r--actionpack/test/fixtures/test/with_partial.html.erb1
-rw-r--r--actionpack/test/fixtures/test/with_partial.text.erb1
-rw-r--r--actionpack/test/fixtures/test/with_xml_template.html.erb1
-rw-r--r--actionpack/test/journey/path/pattern_test.rb46
-rw-r--r--actionpack/test/journey/route_test.rb22
-rw-r--r--actionpack/test/journey/router/strexp_test.rb32
-rw-r--r--actionpack/test/journey/router/utils_test.rb13
-rw-r--r--actionpack/test/journey/router_test.rb193
-rw-r--r--actionpack/test/journey/routes_test.rb10
-rw-r--r--actionpack/test/routing/helper_test.rb14
-rw-r--r--actionview/CHANGELOG.md335
-rw-r--r--actionview/README.rdoc7
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc6
-rw-r--r--actionview/lib/action_view/base.rb12
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb14
-rw-r--r--actionview/lib/action_view/digestor.rb70
-rw-r--r--actionview/lib/action_view/gem_version.rb15
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb51
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb53
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/capture_helper.rb9
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb18
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb18
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb64
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb174
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb167
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb37
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb12
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb8
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb20
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb21
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_field.rb12
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/select.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb1
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb33
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb32
-rw-r--r--actionview/lib/action_view/layouts.rb2
-rw-r--r--actionview/lib/action_view/log_subscriber.rb10
-rw-r--r--actionview/lib/action_view/lookup_context.rb23
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb6
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb100
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb19
-rw-r--r--actionview/lib/action_view/rendering.rb11
-rw-r--r--actionview/lib/action_view/routing_url_for.rb16
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake16
-rw-r--r--actionview/lib/action_view/template.rb4
-rw-r--r--actionview/lib/action_view/template/handlers.rb9
-rw-r--r--actionview/lib/action_view/template/html.rb34
-rw-r--r--actionview/lib/action_view/template/resolver.rb47
-rw-r--r--actionview/lib/action_view/template/text.rb2
-rw-r--r--actionview/lib/action_view/test_case.rb5
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb12
-rw-r--r--actionview/lib/action_view/version.rb11
-rw-r--r--actionview/lib/action_view/view_paths.rb41
-rw-r--r--actionview/test/abstract_unit.rb3
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb48
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb16
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb33
-rw-r--r--actionview/test/actionpack/controller/render_test.rb28
-rw-r--r--actionview/test/active_record_unit.rb2
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb9
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb224
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_iteration.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb1
-rw-r--r--actionview/test/fixtures/digestor/messages/new.html+iphone.erb15
-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/template/asset_tag_helper_test.rb37
-rw-r--r--actionview/test/template/capture_helper_test.rb23
-rw-r--r--actionview/test/template/compiled_templates_test.rb9
-rw-r--r--actionview/test/template/date_helper_test.rb16
-rw-r--r--actionview/test/template/debug_helper_test.rb2
-rw-r--r--actionview/test/template/dependency_tracker_test.rb16
-rw-r--r--actionview/test/template/digestor_test.rb54
-rw-r--r--actionview/test/template/erb_util_test.rb1
-rw-r--r--actionview/test/template/form_collections_helper_test.rb58
-rw-r--r--actionview/test/template/form_helper_test.rb280
-rw-r--r--actionview/test/template/form_options_helper_test.rb35
-rw-r--r--actionview/test/template/form_tag_helper_test.rb18
-rw-r--r--actionview/test/template/html_test.rb17
-rw-r--r--actionview/test/template/javascript_helper_test.rb6
-rw-r--r--actionview/test/template/lookup_context_test.rb14
-rw-r--r--actionview/test/template/number_helper_test.rb45
-rw-r--r--actionview/test/template/output_buffer_test.rb59
-rw-r--r--actionview/test/template/output_safety_helper_test.rb9
-rw-r--r--actionview/test/template/partial_iteration_test.rb33
-rw-r--r--actionview/test/template/render_test.rb49
-rw-r--r--actionview/test/template/sanitize_helper_test.rb2
-rw-r--r--actionview/test/template/tag_helper_test.rb25
-rw-r--r--actionview/test/template/test_case_test.rb3
-rw-r--r--actionview/test/template/test_test.rb18
-rw-r--r--actionview/test/template/text_helper_test.rb27
-rw-r--r--actionview/test/template/text_test.rb17
-rw-r--r--actionview/test/template/translation_helper_test.rb12
-rw-r--r--actionview/test/template/url_helper_test.rb40
-rw-r--r--activemodel/CHANGELOG.md80
-rw-r--r--activemodel/README.rdoc86
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/conversion.rb21
-rw-r--r--activemodel/lib/active_model/dirty.rb51
-rw-r--r--activemodel/lib/active_model/errors.rb9
-rw-r--r--activemodel/lib/active_model/gem_version.rb15
-rw-r--r--activemodel/lib/active_model/locale/en.yml12
-rw-r--r--activemodel/lib/active_model/model.rb6
-rw-r--r--activemodel/lib/active_model/naming.rb12
-rw-r--r--activemodel/lib/active_model/secure_password.rb43
-rw-r--r--activemodel/lib/active_model/serialization.rb16
-rw-r--r--activemodel/lib/active_model/validations.rb10
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb14
-rw-r--r--activemodel/lib/active_model/validations/with.rb4
-rw-r--r--activemodel/lib/active_model/validator.rb18
-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.rb4
-rw-r--r--activemodel/test/cases/dirty_test.rb51
-rw-r--r--activemodel/test/cases/errors_test.rb7
-rw-r--r--activemodel/test/cases/helper.rb2
-rw-r--r--activemodel/test/cases/naming_test.rb6
-rw-r--r--activemodel/test/cases/secure_password_test.rb96
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb49
-rw-r--r--activemodel/test/cases/serializers/xml_serialization_test.rb66
-rw-r--r--activemodel/test/cases/translation_test.rb4
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb7
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb35
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb18
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb1
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb17
-rw-r--r--activemodel/test/cases/validations_test.rb58
-rw-r--r--activemodel/test/models/automobile.rb12
-rw-r--r--activemodel/test/models/user.rb1
-rw-r--r--activemodel/test/models/visitor.rb1
-rw-r--r--activerecord/CHANGELOG.md1941
-rw-r--r--activerecord/README.rdoc23
-rw-r--r--activerecord/Rakefile191
-rw-r--r--activerecord/activerecord.gemspec2
-rw-r--r--activerecord/lib/active_record.rb3
-rw-r--r--activerecord/lib/active_record/aggregations.rb20
-rw-r--r--activerecord/lib/active_record/association_relation.rb4
-rw-r--r--activerecord/lib/active_record/associations.rb93
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb12
-rw-r--r--activerecord/lib/active_record/associations/association.rb6
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb64
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb19
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb36
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb67
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb11
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb81
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb80
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb17
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb34
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb52
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb38
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb33
-rw-r--r--activerecord/lib/active_record/attribute.rb120
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb28
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb66
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb138
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb128
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb21
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb71
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb176
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb60
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb35
-rw-r--r--activerecord/lib/active_record/attribute_set.rb77
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb32
-rw-r--r--activerecord/lib/active_record/attributes.rb122
-rw-r--r--activerecord/lib/active_record/autosave_association.rb77
-rw-r--r--activerecord/lib/active_record/base.rb30
-rw-r--r--activerecord/lib/active_record/callbacks.rb4
-rw-r--r--activerecord/lib/active_record/coders/json.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb112
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb85
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb114
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb47
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb102
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb235
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb214
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb195
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb283
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb257
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb113
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb29
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb202
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb164
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb62
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb403
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb96
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb52
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb85
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb170
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb154
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb121
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb639
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb110
-rw-r--r--activerecord/lib/active_record/connection_handling.rb43
-rw-r--r--activerecord/lib/active_record/core.rb143
-rw-r--r--activerecord/lib/active_record/counter_cache.rb67
-rw-r--r--activerecord/lib/active_record/enum.rb30
-rw-r--r--activerecord/lib/active_record/errors.rb53
-rw-r--r--activerecord/lib/active_record/explain.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb73
-rw-r--r--activerecord/lib/active_record/gem_version.rb15
-rw-r--r--activerecord/lib/active_record/inheritance.rb41
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb50
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb51
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb21
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb87
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb14
-rw-r--r--activerecord/lib/active_record/null_relation.rb24
-rw-r--r--activerecord/lib/active_record/persistence.rb73
-rw-r--r--activerecord/lib/active_record/query_cache.rb6
-rw-r--r--activerecord/lib/active_record/querying.rb9
-rw-r--r--activerecord/lib/active_record/railtie.rb23
-rw-r--r--activerecord/lib/active_record/railties/databases.rake97
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb1
-rw-r--r--activerecord/lib/active_record/reflection.rb350
-rw-r--r--activerecord/lib/active_record/relation.rb79
-rw-r--r--activerecord/lib/active_record/relation/batches.rb1
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb67
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb4
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb73
-rw-r--r--activerecord/lib/active_record/relation/merger.rb13
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb19
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb27
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb145
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb3
-rw-r--r--activerecord/lib/active_record/result.rb25
-rw-r--r--activerecord/lib/active_record/sanitization.rb9
-rw-r--r--activerecord/lib/active_record/schema.rb1
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb63
-rw-r--r--activerecord/lib/active_record/schema_migration.rb11
-rw-r--r--activerecord/lib/active_record/scoping/default.rb15
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb10
-rw-r--r--activerecord/lib/active_record/statement_cache.rb84
-rw-r--r--activerecord/lib/active_record/store.rb29
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb56
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb4
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb6
-rw-r--r--activerecord/lib/active_record/timestamp.rb15
-rw-r--r--activerecord/lib/active_record/transactions.rb28
-rw-r--r--activerecord/lib/active_record/type.rb20
-rw-r--r--activerecord/lib/active_record/type/binary.rb40
-rw-r--r--activerecord/lib/active_record/type/boolean.rb19
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb43
-rw-r--r--activerecord/lib/active_record/type/decimal.rb40
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/float.rb19
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb19
-rw-r--r--activerecord/lib/active_record/type/integer.rb23
-rw-r--r--activerecord/lib/active_record/type/mutable.rb16
-rw-r--r--activerecord/lib/active_record/type/numeric.rb36
-rw-r--r--activerecord/lib/active_record/type/serialized.rb51
-rw-r--r--activerecord/lib/active_record/type/string.rb36
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb26
-rw-r--r--activerecord/lib/active_record/type/time_value.rb38
-rw-r--r--activerecord/lib/active_record/type/type_map.rb48
-rw-r--r--activerecord/lib/active_record/type/value.rb94
-rw-r--r--activerecord/lib/active_record/validations.rb37
-rw-r--r--activerecord/lib/active_record/validations/presence.rb2
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb25
-rw-r--r--activerecord/lib/active_record/version.rb11
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb10
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb1
-rw-r--r--activerecord/test/cases/adapter_test.rb38
-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.rb102
-rw-r--r--activerecord/test/cases/adapters/mysql/consistency_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb108
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb38
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb34
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb174
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb76
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb30
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb76
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb105
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb52
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb248
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb85
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb63
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb26
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb72
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb156
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb44
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb99
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb96
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb71
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb131
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb134
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb74
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb75
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb15
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb208
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb32
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb22
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb1
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb25
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb406
-rw-r--r--activerecord/test/cases/ar_schema_test.rb26
-rw-r--r--activerecord/test/cases/associations/association_scope_test.rb5
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb92
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb27
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb26
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb6
-rw-r--r--activerecord/test/cases/associations/eager_test.rb152
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb90
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb117
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb88
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb14
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb19
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb13
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb25
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb3
-rw-r--r--activerecord/test/cases/associations/required_test.rb82
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb124
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb1
-rw-r--r--activerecord/test/cases/attribute_methods/serialization_test.rb29
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb173
-rw-r--r--activerecord/test/cases/attribute_set_test.rb165
-rw-r--r--activerecord/test/cases/attribute_test.rb142
-rw-r--r--activerecord/test/cases/attributes_test.rb111
-rw-r--r--activerecord/test/cases/autosave_association_test.rb52
-rw-r--r--activerecord/test/cases/base_test.rb141
-rw-r--r--activerecord/test/cases/binary_test.rb6
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb22
-rw-r--r--activerecord/test/cases/calculations_test.rb30
-rw-r--r--activerecord/test/cases/column_definition_test.rb62
-rw-r--r--activerecord/test/cases/column_test.rb123
-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/connection_handler_test.rb127
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb204
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb61
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb101
-rw-r--r--activerecord/test/cases/connection_pool_test.rb32
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb27
-rw-r--r--activerecord/test/cases/core_test.rb68
-rw-r--r--activerecord/test/cases/counter_cache_test.rb26
-rw-r--r--activerecord/test/cases/defaults_test.rb11
-rw-r--r--activerecord/test/cases/dirty_test.rb61
-rw-r--r--activerecord/test/cases/disconnected_test.rb2
-rw-r--r--activerecord/test/cases/dup_test.rb21
-rw-r--r--activerecord/test/cases/enum_test.rb64
-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_test.rb53
-rw-r--r--activerecord/test/cases/fixtures_test.rb21
-rw-r--r--activerecord/test/cases/helper.rb19
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb2
-rw-r--r--activerecord/test/cases/inheritance_test.rb10
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb2
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb25
-rw-r--r--activerecord/test/cases/locking_test.rb11
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb7
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb48
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb16
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb162
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb3
-rw-r--r--activerecord/test/cases/migration/columns_test.rb27
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb42
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb27
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb242
-rw-r--r--activerecord/test/cases/migration/index_test.rb101
-rw-r--r--activerecord/test/cases/migration/logger_test.rb5
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb53
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb3
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb5
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb20
-rw-r--r--activerecord/test/cases/migration_test.rb67
-rw-r--r--activerecord/test/cases/migrator_test.rb5
-rw-r--r--activerecord/test/cases/modules_test.rb30
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb4
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb42
-rw-r--r--activerecord/test/cases/persistence_test.rb95
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb4
-rw-r--r--activerecord/test/cases/primary_keys_test.rb83
-rw-r--r--activerecord/test/cases/query_cache_test.rb8
-rw-r--r--activerecord/test/cases/quoting_test.rb46
-rw-r--r--activerecord/test/cases/reaper_test.rb20
-rw-r--r--activerecord/test/cases/reflection_test.rb117
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb2
-rw-r--r--activerecord/test/cases/relation/merging_test.rb43
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb21
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb4
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb95
-rw-r--r--activerecord/test/cases/relation/where_test.rb33
-rw-r--r--activerecord/test/cases/relation_test.rb32
-rw-r--r--activerecord/test/cases/relations_test.rb191
-rw-r--r--activerecord/test/cases/result_test.rb44
-rw-r--r--activerecord/test/cases/sanitize_test.rb27
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb72
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb18
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb3
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb13
-rw-r--r--activerecord/test/cases/serialization_test.rb27
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb150
-rw-r--r--activerecord/test/cases/statement_cache_test.rb64
-rw-r--r--activerecord/test/cases/store_test.rb31
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb49
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb6
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb2
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb2
-rw-r--r--activerecord/test/cases/test_case.rb33
-rw-r--r--activerecord/test/cases/timestamp_test.rb77
-rw-r--r--activerecord/test/cases/transactions_test.rb47
-rw-r--r--activerecord/test/cases/type/decimal_test.rb38
-rw-r--r--activerecord/test/cases/type/string_test.rb36
-rw-r--r--activerecord/test/cases/type/type_map_test.rb130
-rw-r--r--activerecord/test/cases/types_test.rb163
-rw-r--r--activerecord/test/cases/unconnected_test.rb2
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb43
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb3
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb47
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb5
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb2
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb2
-rw-r--r--activerecord/test/cases/validations_test.rb47
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb18
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb52
-rw-r--r--activerecord/test/config.example.yml30
-rw-r--r--activerecord/test/fixtures/books.yml2
-rw-r--r--activerecord/test/fixtures/computers.yml1
-rw-r--r--activerecord/test/fixtures/fk_test_has_pk.yml2
-rw-r--r--activerecord/test/fixtures/pirates.yml3
-rw-r--r--activerecord/test/fixtures/posts.yml2
-rw-r--r--activerecord/test/fixtures/topics.yml10
-rw-r--r--activerecord/test/fixtures/uuid_children.yml3
-rw-r--r--activerecord/test/fixtures/uuid_parents.yml2
-rw-r--r--activerecord/test/models/author.rb2
-rw-r--r--activerecord/test/models/category.rb1
-rw-r--r--activerecord/test/models/club.rb9
-rw-r--r--activerecord/test/models/college.rb5
-rw-r--r--activerecord/test/models/column.rb3
-rw-r--r--activerecord/test/models/comment.rb15
-rw-r--r--activerecord/test/models/company_in_module.rb18
-rw-r--r--activerecord/test/models/contact.rb1
-rw-r--r--activerecord/test/models/developer.rb10
-rw-r--r--activerecord/test/models/face.rb2
-rw-r--r--activerecord/test/models/man.rb1
-rw-r--r--activerecord/test/models/movie.rb2
-rw-r--r--activerecord/test/models/owner.rb12
-rw-r--r--activerecord/test/models/person.rb13
-rw-r--r--activerecord/test/models/pirate.rb5
-rw-r--r--activerecord/test/models/post.rb25
-rw-r--r--activerecord/test/models/publisher.rb2
-rw-r--r--activerecord/test/models/publisher/article.rb4
-rw-r--r--activerecord/test/models/publisher/magazine.rb3
-rw-r--r--activerecord/test/models/reader.rb2
-rw-r--r--activerecord/test/models/record.rb2
-rw-r--r--activerecord/test/models/ship.rb2
-rw-r--r--activerecord/test/models/student.rb1
-rw-r--r--activerecord/test/models/tag.rb4
-rw-r--r--activerecord/test/models/tagging.rb2
-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/postgresql_specific_schema.rb24
-rw-r--r--activerecord/test/schema/schema.rb72
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb6
-rw-r--r--activerecord/test/support/connection_helper.rb14
-rw-r--r--activerecord/test/support/ddl_helper.rb8
-rw-r--r--activerecord/test/support/schema_dumping_helper.rb11
-rw-r--r--activesupport/CHANGELOG.md666
-rw-r--r--activesupport/README.rdoc7
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb8
-rw-r--r--activesupport/lib/active_support/cache.rb34
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb5
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb44
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb39
-rw-r--r--activesupport/lib/active_support/callbacks.rb46
-rw-r--r--activesupport/lib/active_support/concern.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/class/delegating_attributes.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/digest/uuid.rb51
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb70
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb23
-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.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb38
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/object/itself.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_json.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb63
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_query.rb66
-rw-r--r--activesupport/lib/active_support/core_ext/object/with_options.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/string/access.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb42
-rw-r--r--activesupport/lib/active_support/core_ext/thread.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb2
-rw-r--r--activesupport/lib/active_support/dependencies.rb17
-rw-r--r--activesupport/lib/active_support/duration.rb7
-rw-r--r--activesupport/lib/active_support/file_watcher.rb36
-rw-r--r--activesupport/lib/active_support/gem_version.rb15
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb21
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb4
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb2
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb56
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb3
-rw-r--r--activesupport/lib/active_support/notifications.rb9
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb17
-rw-r--r--activesupport/lib/active_support/number_helper.rb15
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb4
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb9
-rw-r--r--activesupport/lib/active_support/option_merger.rb2
-rw-r--r--activesupport/lib/active_support/ordered_hash.rb4
-rw-r--r--activesupport/lib/active_support/subscriber.rb11
-rw-r--r--activesupport/lib/active_support/test_case.rb16
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb2
-rw-r--r--activesupport/lib/active_support/testing/declarative.rb26
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb22
-rw-r--r--activesupport/lib/active_support/testing/tagged_logging.rb2
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb32
-rw-r--r--activesupport/lib/active_support/time.rb2
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb17
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb148
-rw-r--r--activesupport/lib/active_support/version.rb11
-rw-r--r--activesupport/test/abstract_unit.rb2
-rw-r--r--activesupport/test/caching_test.rb49
-rw-r--r--activesupport/test/clean_backtrace_test.rb5
-rw-r--r--activesupport/test/concern_test.rb10
-rw-r--r--activesupport/test/constantize_test_cases.rb11
-rw-r--r--activesupport/test/core_ext/array/access_test.rb30
-rw-r--r--activesupport/test/core_ext/array/conversions_test.rb197
-rw-r--r--activesupport/test/core_ext/array/extract_options_test.rb45
-rw-r--r--activesupport/test/core_ext/array/grouping_test.rb126
-rw-r--r--activesupport/test/core_ext/array/prepend_append_test.rb12
-rw-r--r--activesupport/test/core_ext/array/wrap_test.rb77
-rw-r--r--activesupport/test/core_ext/array_ext_test.rb451
-rw-r--r--activesupport/test/core_ext/class/delegating_attributes_test.rb34
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb18
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb17
-rw-r--r--activesupport/test/core_ext/digest/uuid_test.rb24
-rw-r--r--activesupport/test/core_ext/duration_test.rb22
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb32
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb61
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb146
-rw-r--r--activesupport/test/core_ext/kernel/concern_test.rb13
-rw-r--r--activesupport/test/core_ext/kernel_test.rb38
-rw-r--r--activesupport/test/core_ext/load_error_test.rb11
-rw-r--r--activesupport/test/core_ext/module/concerning_test.rb58
-rw-r--r--activesupport/test/core_ext/module_test.rb2
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb52
-rw-r--r--activesupport/test/core_ext/object/acts_like_test.rb33
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb (renamed from activesupport/test/core_ext/blank_test.rb)0
-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.rb9
-rw-r--r--activesupport/test/core_ext/object/instance_variables_test.rb31
-rw-r--r--activesupport/test/core_ext/object/itself_test.rb9
-rw-r--r--activesupport/test/core_ext/object/json_cherry_pick_test.rb42
-rw-r--r--activesupport/test/core_ext/object/json_test.rb9
-rw-r--r--activesupport/test/core_ext/object/to_param_test.rb18
-rw-r--r--activesupport/test/core_ext/object/to_query_test.rb22
-rw-r--r--activesupport/test/core_ext/object/try_test.rb (renamed from activesupport/test/core_ext/object_and_class_ext_test.rb)67
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb8
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb85
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb11
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb78
-rw-r--r--activesupport/test/core_ext/uri_ext_test.rb2
-rw-r--r--activesupport/test/dependencies_test.rb28
-rw-r--r--activesupport/test/deprecation_test.rb17
-rw-r--r--activesupport/test/i18n_test.rb1
-rw-r--r--activesupport/test/inflector_test.rb24
-rw-r--r--activesupport/test/inflector_test_cases.rb10
-rw-r--r--activesupport/test/json/decoding_test.rb30
-rw-r--r--activesupport/test/json/encoding_test.rb84
-rw-r--r--activesupport/test/key_generator_test.rb30
-rw-r--r--activesupport/test/multibyte_conformance_test.rb (renamed from activesupport/test/multibyte_conformance.rb)16
-rw-r--r--activesupport/test/multibyte_proxy_test.rb34
-rw-r--r--activesupport/test/number_helper_i18n_test.rb16
-rw-r--r--activesupport/test/number_helper_test.rb7
-rw-r--r--activesupport/test/option_merger_test.rb9
-rw-r--r--activesupport/test/ordered_hash_test.rb5
-rw-r--r--activesupport/test/subscriber_test.rb26
-rw-r--r--activesupport/test/test_test.rb20
-rw-r--r--activesupport/test/time_zone_test.rb46
-rw-r--r--activesupport/test/time_zone_test_helpers.rb16
-rw-r--r--activesupport/test/xml_mini_test.rb59
-rwxr-xr-xci/travis.rb2
-rw-r--r--guides/CHANGELOG.md26
-rw-r--r--guides/Rakefile4
-rw-r--r--guides/assets/images/getting_started/article_with_comments.pngbin25229 -> 15190 bytes
-rw-r--r--guides/assets/images/getting_started/challenge.pngbin30248 -> 21690 bytes
-rw-r--r--guides/assets/images/getting_started/confirm_dialog.pngbin26420 -> 18809 bytes
-rw-r--r--guides/assets/images/getting_started/forbidden_attributes_for_new_article.pngbin15598 -> 10783 bytes
-rw-r--r--guides/assets/images/getting_started/form_with_errors.pngbin18076 -> 12447 bytes
-rw-r--r--guides/assets/images/getting_started/index_action_with_edit_link.pngbin18024 -> 10209 bytes
-rw-r--r--guides/assets/images/getting_started/new_article.pngbin9387 -> 3579 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.jpgbin293182 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.pngbin0 -> 94542 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_controller.pngbin6108 -> 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.pngbin2991 -> 2965 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_articles_new.pngbin10138 -> 6174 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_articles.pngbin7594 -> 5327 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_articles.pngbin7858 -> 5481 bytes
-rw-r--r--guides/assets/javascripts/guides.js6
-rw-r--r--guides/assets/stylesheets/main.css5
-rw-r--r--guides/bug_report_templates/action_controller_master.rb3
-rw-r--r--guides/bug_report_templates/active_record_master.rb2
-rw-r--r--guides/code/getting_started/.gitignore16
-rw-r--r--guides/code/getting_started/Gemfile40
-rw-r--r--guides/code/getting_started/Gemfile.lock126
-rw-r--r--guides/code/getting_started/README.rdoc28
-rw-r--r--guides/code/getting_started/Rakefile6
-rw-r--r--guides/code/getting_started/app/assets/javascripts/application.js15
-rw-r--r--guides/code/getting_started/app/assets/javascripts/comments.js.coffee3
-rw-r--r--guides/code/getting_started/app/assets/javascripts/posts.js.coffee3
-rw-r--r--guides/code/getting_started/app/assets/javascripts/welcome.js.coffee3
-rw-r--r--guides/code/getting_started/app/assets/stylesheets/application.css13
-rw-r--r--guides/code/getting_started/app/assets/stylesheets/comments.css.scss3
-rw-r--r--guides/code/getting_started/app/assets/stylesheets/posts.css.scss3
-rw-r--r--guides/code/getting_started/app/assets/stylesheets/welcome.css.scss3
-rw-r--r--guides/code/getting_started/app/controllers/application_controller.rb5
-rw-r--r--guides/code/getting_started/app/controllers/comments_controller.rb23
-rw-r--r--guides/code/getting_started/app/controllers/concerns/.keep0
-rw-r--r--guides/code/getting_started/app/controllers/posts_controller.rb53
-rw-r--r--guides/code/getting_started/app/controllers/welcome_controller.rb4
-rw-r--r--guides/code/getting_started/app/helpers/application_helper.rb2
-rw-r--r--guides/code/getting_started/app/helpers/comments_helper.rb2
-rw-r--r--guides/code/getting_started/app/helpers/posts_helper.rb2
-rw-r--r--guides/code/getting_started/app/helpers/welcome_helper.rb2
-rw-r--r--guides/code/getting_started/app/mailers/.keep0
-rw-r--r--guides/code/getting_started/app/models/.keep0
-rw-r--r--guides/code/getting_started/app/models/comment.rb3
-rw-r--r--guides/code/getting_started/app/models/concerns/.keep0
-rw-r--r--guides/code/getting_started/app/models/post.rb7
-rw-r--r--guides/code/getting_started/app/views/comments/_comment.html.erb15
-rw-r--r--guides/code/getting_started/app/views/comments/_form.html.erb13
-rw-r--r--guides/code/getting_started/app/views/layouts/application.html.erb14
-rw-r--r--guides/code/getting_started/app/views/posts/_form.html.erb27
-rw-r--r--guides/code/getting_started/app/views/posts/edit.html.erb5
-rw-r--r--guides/code/getting_started/app/views/posts/index.html.erb21
-rw-r--r--guides/code/getting_started/app/views/posts/new.html.erb5
-rw-r--r--guides/code/getting_started/app/views/posts/show.html.erb18
-rw-r--r--guides/code/getting_started/app/views/welcome/index.html.erb4
-rwxr-xr-xguides/code/getting_started/bin/bundle4
-rwxr-xr-xguides/code/getting_started/bin/rails4
-rwxr-xr-xguides/code/getting_started/bin/rake4
-rw-r--r--guides/code/getting_started/config.ru4
-rw-r--r--guides/code/getting_started/config/application.rb18
-rw-r--r--guides/code/getting_started/config/boot.rb4
-rw-r--r--guides/code/getting_started/config/database.yml25
-rw-r--r--guides/code/getting_started/config/environment.rb5
-rw-r--r--guides/code/getting_started/config/environments/development.rb30
-rw-r--r--guides/code/getting_started/config/environments/production.rb80
-rw-r--r--guides/code/getting_started/config/environments/test.rb36
-rw-r--r--guides/code/getting_started/config/initializers/backtrace_silencers.rb7
-rw-r--r--guides/code/getting_started/config/initializers/filter_parameter_logging.rb4
-rw-r--r--guides/code/getting_started/config/initializers/inflections.rb16
-rw-r--r--guides/code/getting_started/config/initializers/locale.rb9
-rw-r--r--guides/code/getting_started/config/initializers/mime_types.rb5
-rw-r--r--guides/code/getting_started/config/initializers/secret_token.rb12
-rw-r--r--guides/code/getting_started/config/initializers/session_store.rb3
-rw-r--r--guides/code/getting_started/config/initializers/wrap_parameters.rb14
-rw-r--r--guides/code/getting_started/config/locales/en.yml23
-rw-r--r--guides/code/getting_started/config/routes.rb7
-rw-r--r--guides/code/getting_started/db/migrate/20130122042648_create_posts.rb10
-rw-r--r--guides/code/getting_started/db/migrate/20130122045842_create_comments.rb11
-rw-r--r--guides/code/getting_started/db/schema.rb33
-rw-r--r--guides/code/getting_started/db/seeds.rb7
-rw-r--r--guides/code/getting_started/lib/assets/.keep0
-rw-r--r--guides/code/getting_started/lib/tasks/.keep0
-rw-r--r--guides/code/getting_started/log/.keep0
-rw-r--r--guides/code/getting_started/public/404.html60
-rw-r--r--guides/code/getting_started/public/422.html60
-rw-r--r--guides/code/getting_started/public/500.html59
-rw-r--r--guides/code/getting_started/public/favicon.ico0
-rw-r--r--guides/code/getting_started/public/robots.txt5
-rw-r--r--guides/code/getting_started/test/controllers/.keep0
-rw-r--r--guides/code/getting_started/test/controllers/comments_controller_test.rb7
-rw-r--r--guides/code/getting_started/test/controllers/posts_controller_test.rb7
-rw-r--r--guides/code/getting_started/test/controllers/welcome_controller_test.rb9
-rw-r--r--guides/code/getting_started/test/fixtures/.keep0
-rw-r--r--guides/code/getting_started/test/fixtures/comments.yml11
-rw-r--r--guides/code/getting_started/test/fixtures/posts.yml9
-rw-r--r--guides/code/getting_started/test/helpers/.keep0
-rw-r--r--guides/code/getting_started/test/helpers/comments_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/helpers/posts_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/helpers/welcome_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/integration/.keep0
-rw-r--r--guides/code/getting_started/test/mailers/.keep0
-rw-r--r--guides/code/getting_started/test/models/.keep0
-rw-r--r--guides/code/getting_started/test/models/comment_test.rb7
-rw-r--r--guides/code/getting_started/test/models/post_test.rb7
-rw-r--r--guides/code/getting_started/test/test_helper.rb15
-rw-r--r--guides/code/getting_started/vendor/assets/javascripts/.keep0
-rw-r--r--guides/code/getting_started/vendor/assets/stylesheets/.keep0
-rw-r--r--guides/rails_guides.rb4
-rw-r--r--guides/rails_guides/helpers.rb2
-rw-r--r--guides/rails_guides/levenshtein.rb50
-rw-r--r--guides/rails_guides/markdown.rb13
-rw-r--r--guides/source/2_3_release_notes.md6
-rw-r--r--guides/source/3_0_release_notes.md8
-rw-r--r--guides/source/3_1_release_notes.md2
-rw-r--r--guides/source/3_2_release_notes.md4
-rw-r--r--guides/source/4_1_release_notes.md47
-rw-r--r--guides/source/4_2_release_notes.md402
-rw-r--r--guides/source/_license.html.erb2
-rw-r--r--guides/source/_welcome.html.erb7
-rw-r--r--guides/source/action_controller_overview.md118
-rw-r--r--guides/source/action_mailer_basics.md53
-rw-r--r--guides/source/action_view_overview.md237
-rw-r--r--guides/source/active_model_basics.md23
-rw-r--r--guides/source/active_record_basics.md31
-rw-r--r--guides/source/active_record_callbacks.md21
-rw-r--r--guides/source/active_record_migrations.md (renamed from guides/source/migrations.md)386
-rw-r--r--guides/source/active_record_postgresql.md437
-rw-r--r--guides/source/active_record_querying.md542
-rw-r--r--guides/source/active_record_validations.md18
-rw-r--r--guides/source/active_support_core_extensions.md102
-rw-r--r--guides/source/active_support_instrumentation.md5
-rw-r--r--guides/source/api_documentation_guidelines.md100
-rw-r--r--guides/source/asset_pipeline.md127
-rw-r--r--guides/source/association_basics.md85
-rw-r--r--guides/source/caching_with_rails.md30
-rw-r--r--guides/source/command_line.md131
-rw-r--r--guides/source/configuring.md123
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md285
-rw-r--r--guides/source/credits.html.erb2
-rw-r--r--guides/source/debugging_rails_applications.md738
-rw-r--r--guides/source/development_dependencies_install.md19
-rw-r--r--guides/source/documents.yaml11
-rw-r--r--guides/source/engines.md372
-rw-r--r--guides/source/form_helpers.md99
-rw-r--r--guides/source/generators.md55
-rw-r--r--guides/source/getting_started.md552
-rw-r--r--guides/source/i18n.md164
-rw-r--r--guides/source/index.html.erb1
-rw-r--r--guides/source/initialization.md169
-rw-r--r--guides/source/layout.html.erb7
-rw-r--r--guides/source/layouts_and_rendering.md67
-rw-r--r--guides/source/maintenance_policy.md27
-rw-r--r--guides/source/nested_model_forms.md6
-rw-r--r--guides/source/plugins.md22
-rw-r--r--guides/source/rails_application_templates.md28
-rw-r--r--guides/source/rails_on_rack.md15
-rw-r--r--guides/source/routing.md174
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md22
-rw-r--r--guides/source/security.md64
-rw-r--r--guides/source/testing.md218
-rw-r--r--guides/source/upgrading_ruby_on_rails.md271
-rw-r--r--guides/source/working_with_javascript_in_rails.md36
-rw-r--r--guides/w3c_validator.rb2
-rw-r--r--rails.gemspec2
-rw-r--r--railties/CHANGELOG.md337
-rw-r--r--railties/RDOC_MAIN.rdoc2
-rw-r--r--railties/README.rdoc6
-rw-r--r--railties/lib/rails.rb12
-rw-r--r--railties/lib/rails/app_rails_loader.rb6
-rw-r--r--railties/lib/rails/application.rb110
-rw-r--r--railties/lib/rails/application/bootstrap.rb6
-rw-r--r--railties/lib/rails/application/configuration.rb21
-rw-r--r--railties/lib/rails/application/finisher.rb2
-rw-r--r--railties/lib/rails/commands/commands_tasks.rb7
-rw-r--r--railties/lib/rails/commands/console.rb36
-rw-r--r--railties/lib/rails/commands/dbconsole.rb2
-rw-r--r--railties/lib/rails/commands/plugin.rb2
-rw-r--r--railties/lib/rails/commands/server.rb31
-rw-r--r--railties/lib/rails/engine.rb7
-rw-r--r--railties/lib/rails/gem_version.rb15
-rw-r--r--railties/lib/rails/generators.rb83
-rw-r--r--railties/lib/rails/generators/actions.rb30
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb5
-rw-r--r--railties/lib/rails/generators/app_base.rb59
-rw-r--r--railties/lib/rails/generators/base.rb4
-rw-r--r--railties/lib/rails/generators/erb.rb4
-rw-r--r--railties/lib/rails/generators/erb/controller/controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb11
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb4
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/new.html.erb2
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb4
-rw-r--r--railties/lib/rails/generators/model_helpers.rb28
-rw-r--r--railties/lib/rails/generators/named_base.rb7
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb29
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup28
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml30
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml15
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml27
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml28
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt24
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb2
-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/gitignore2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb3
-rw-r--r--railties/lib/rails/generators/rails/controller/USAGE1
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb12
-rw-r--r--railties/lib/rails/generators/rails/helper/USAGE4
-rw-r--r--railties/lib/rails/generators/rails/model/USAGE12
-rw-r--r--railties/lib/rails/generators/rails/model/model_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb22
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile16
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Rakefile4
-rw-r--r--railties/lib/rails/generators/rails/scaffold/USAGE8
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb4
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/helper/helper_generator.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb6
-rw-r--r--railties/lib/rails/generators/testing/assertions.rb2
-rw-r--r--railties/lib/rails/generators/testing/behaviour.rb17
-rw-r--r--railties/lib/rails/info.rb2
-rw-r--r--railties/lib/rails/info_controller.rb2
-rw-r--r--railties/lib/rails/mailers_controller.rb6
-rw-r--r--railties/lib/rails/paths.rb2
-rw-r--r--railties/lib/rails/rack.rb2
-rw-r--r--railties/lib/rails/rack/log_tailer.rb4
-rw-r--r--railties/lib/rails/rack/logger.rb2
-rw-r--r--railties/lib/rails/railtie.rb22
-rw-r--r--railties/lib/rails/ruby_version_check.rb2
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb39
-rw-r--r--railties/lib/rails/tasks/framework.rake4
-rw-r--r--railties/lib/rails/tasks/statistics.rake11
-rw-r--r--railties/lib/rails/test_help.rb1
-rw-r--r--railties/lib/rails/test_unit/sub_test_task.rb4
-rw-r--r--railties/lib/rails/version.rb12
-rw-r--r--railties/test/abstract_unit.rb23
-rw-r--r--railties/test/app_rails_loader_test.rb31
-rw-r--r--railties/test/application/assets_test.rb40
-rw-r--r--railties/test/application/configuration/base_test.rb37
-rw-r--r--railties/test/application/configuration/custom_test.rb15
-rw-r--r--railties/test/application/configuration_test.rb335
-rw-r--r--railties/test/application/initializers/frameworks_test.rb8
-rw-r--r--railties/test/application/initializers/i18n_test.rb20
-rw-r--r--railties/test/application/mailer_previews_test.rb66
-rw-r--r--railties/test/application/middleware_test.rb2
-rw-r--r--railties/test/application/multiple_applications_test.rb68
-rw-r--r--railties/test/application/rack/logger_test.rb8
-rw-r--r--railties/test/application/rake/dbs_test.rb81
-rw-r--r--railties/test/application/rake/migrations_test.rb17
-rw-r--r--railties/test/application/rake/notes_test.rb125
-rw-r--r--railties/test/application/rake_test.rb11
-rw-r--r--railties/test/application/test_test.rb91
-rw-r--r--railties/test/commands/console_test.rb88
-rw-r--r--railties/test/commands/dbconsole_test.rb112
-rw-r--r--railties/test/engine_test.rb11
-rw-r--r--railties/test/generators/actions_test.rb25
-rw-r--r--railties/test/generators/app_generator_test.rb170
-rw-r--r--railties/test/generators/argv_scrubber_test.rb2
-rw-r--r--railties/test/generators/controller_generator_test.rb9
-rw-r--r--railties/test/generators/generators_test_helper.rb10
-rw-r--r--railties/test/generators/helper_generator_test.rb15
-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.rb1
-rw-r--r--railties/test/generators/namespaced_generators_test.rb7
-rw-r--r--railties/test/generators/plugin_generator_test.rb78
-rw-r--r--railties/test/generators/resource_generator_test.rb7
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb17
-rw-r--r--railties/test/generators/scaffold_generator_test.rb4
-rw-r--r--railties/test/generators/shared_generator_tests.rb13
-rw-r--r--railties/test/generators_test.rb23
-rw-r--r--railties/test/isolation/abstract_unit.rb27
-rw-r--r--railties/test/path_generation_test.rb88
-rw-r--r--railties/test/paths_test.rb1
-rw-r--r--railties/test/rack_logger_test.rb4
-rw-r--r--railties/test/rails_info_controller_test.rb1
-rw-r--r--railties/test/rails_info_test.rb10
-rw-r--r--railties/test/railties/engine_test.rb43
-rw-r--r--railties/test/railties/mounted_engine_test.rb10
-rw-r--r--railties/test/railties/railtie_test.rb8
-rw-r--r--railties/test/version_test.rb12
-rw-r--r--tasks/release.rb50
-rw-r--r--version.rb9
1212 files changed, 32095 insertions, 18736 deletions
diff --git a/.travis.yml b/.travis.yml
index 4233b136a8..43b08044d3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,20 +5,25 @@ before_install:
rvm:
- 1.9.3
- 2.0.0
- - 2.1.0
+ - 2.1
+ - ruby-head
- rbx-2
- jruby
env:
- "GEM=railties"
- - "GEM=ap,am,amo,as,av"
+ - "GEM=ap"
+ - "GEM=am,amo,as,av"
- "GEM=ar:mysql"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
- "GEM=ar:postgresql"
matrix:
allow_failures:
+ - rvm: 1.9.3
+ env: "GEM=ar:mysql"
- rvm: rbx-2
- rvm: jruby
+ - rvm: ruby-head
fast_finish: true
notifications:
email: false
@@ -35,4 +40,3 @@ notifications:
bundler_args: --path vendor/bundle --without test
services:
- memcached
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 19b7b638b6..9ba2e53ef2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -12,5 +12,6 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the tea
* If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code.
-Thanks! :heart: :heart: :heart: <br />
+Thanks! :heart: :heart: :heart:
+
Rails Team
diff --git a/Gemfile b/Gemfile
index 753b1d423a..04cef17458 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,11 +7,19 @@ gemspec
# ensure correct loading order
gem 'mocha', '~> 0.14', require: false
+gem 'rack', github: 'rack/rack'
gem 'rack-cache', '~> 1.2'
-gem 'bcrypt-ruby', '~> 3.1.2'
gem 'jquery-rails', '~> 3.1.0'
-gem 'turbolinks'
+gem 'turbolinks', github: 'rails/turbolinks', branch: 'master'
gem 'coffee-rails', '~> 4.0.0'
+gem 'arel', github: 'rails/arel', branch: 'master'
+gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
+gem 'i18n', github: 'svenfuchs/i18n', branch: 'master'
+
+# 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
@@ -19,7 +27,7 @@ gem 'uglifier', '>= 1.3.0', require: false
group :doc do
gem 'sdoc', '~> 0.4.0'
- gem 'redcarpet', '~> 2.2.2', platforms: :ruby
+ gem 'redcarpet', '~> 3.1.2', platforms: :ruby
gem 'w3c_validators'
gem 'kindlerb'
end
@@ -32,6 +40,9 @@ local_gemfile = File.dirname(__FILE__) + "/.Gemfile"
instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
+ # FIX: Our test suite isn't ready to run in random order yet
+ gem 'minitest', '< 5.3.4'
+
platforms :mri_19 do
gem 'ruby-prof', '~> 0.11.2'
end
@@ -76,10 +87,16 @@ platforms :jruby do
end
end
+platforms :rbx do
+ # The rubysl-yaml gem doesn't ship with Psych by default
+ # as it needs libyaml that isn't always available.
+ gem 'psych', '~> 2.0'
+end
+
# gems that are necessary for ActiveRecord tests with Oracle database
if ENV['ORACLE_ENHANCED']
platforms :ruby do
- gem 'ruby-oci8', '>= 2.0.4'
+ gem 'ruby-oci8', '~> 2.1'
end
gem 'activerecord-oracle_enhanced-adapter', github: 'rsim/oracle-enhanced', branch: 'master'
end
diff --git a/RAILS_VERSION b/RAILS_VERSION
index 78dae579e8..59e61f95df 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-4.1.0.beta1
+4.2.0.alpha
diff --git a/README.md b/README.md
index 7f079536d2..d786914d6d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
-[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)
+[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller)
pattern.
Understanding the MVC pattern is key to understanding Rails. MVC divides your
@@ -67,7 +67,7 @@ independently outside Rails.
* [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html)
* [Ruby on Rails Guides](http://guides.rubyonrails.org)
* [The API Documentation](http://api.rubyonrails.org)
- * [Ruby on Rails Tutorial](http://ruby.railstutorial.org/ruby-on-rails-tutorial-book)
+ * [Ruby on Rails Tutorial](http://www.railstutorial.org/book)
## Contributing
@@ -76,7 +76,7 @@ We encourage you to contribute to Ruby on Rails! Please check out the
## Code Status
-* [![Build Status](https://travis-ci.org/rails/rails.png?branch=master)](https://travis-ci.org/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 664505f60d..f8c40f0b02 100644
--- a/RELEASING_RAILS.rdoc
+++ b/RELEASING_RAILS.rdoc
@@ -25,6 +25,18 @@ for Rails. You can check the status of his tests here:
Do not release with Red AWDwR tests.
+=== Are the supported plugins working? If not, make it work.
+
+Some Rails plugins are important and need to be supported until Rails 5.
+As these plugins are outside the Rails repository it is easy to break then without knowing
+after some refactoring or bug fix, so it is important to check if the following plugins
+are working with the versions that will be released:
+
+* https://github.com/rails/protected_attributes
+* https://github.com/rails/activerecord-deprecated_finders
+
+Do not release red plugins tests.
+
=== Do we have any Git dependencies? If so, contact those authors.
Having Git dependencies indicates that we depend on unreleased code.
@@ -158,7 +170,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 +215,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 07d44fc94b..0737afd089 100644
--- a/Rakefile
+++ b/Rakefile
@@ -45,36 +45,9 @@ else
Rails::API::StableTask.new('rdoc')
end
-desc 'Bump all versions to match version.rb'
-task :update_versions do
- require File.dirname(__FILE__) + "/version"
+desc 'Bump all versions to match RAILS_VERSION'
+task :update_versions => "all:update_versions"
- File.open("RAILS_VERSION", "w") do |f|
- f.puts Rails::VERSION::STRING
- end
-
- constants = {
- "activesupport" => "ActiveSupport",
- "activemodel" => "ActiveModel",
- "actionpack" => "ActionPack",
- "actionview" => "ActionView",
- "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
-end
-
-#
# We have a webhook configured in GitHub that gets invoked after pushes.
# This hook triggers the following tasks:
#
@@ -84,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 5a61746700..ab93745f60 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,61 +1,30 @@
-* Support the use of underscored symbols when registering interceptors and
- observers like we do elsewhere within Rails.
+* Deprecate `*_path` helpers in email views. When used they generate
+ non-working links and are not the intention of most developers. Instead
+ we recommend to use `*_url` helper.
- *Andrew White*
+ *Richard Schneeman*
-* Add the ability to intercept emails before previewing in a similar fashion
- to how emails can be intercepted before delivery.
+* Raise an exception when attachments are added after `mail` was called.
+ This is a safeguard to prevent invalid emails.
- Fixes #13622.
+ Fixes #16163.
- Example:
-
- class CSSInlineStyler
- def self.previewing_email(message)
- # inline CSS styles
- end
- end
-
- ActionMailer::Base.register_preview_interceptor CSSInlineStyler
-
- *Andrew White*
-
-* Add mailer previews feature based on 37 Signals mail_view gem.
-
- *Andrew White*
+ *Yves Senn*
-* Calling `mail()` without arguments serves as getter for the current mail
- message and keeps previously set headers.
+* Add `config.action_mailer.show_previews` configuration option.
- Fixes #13090.
+ This config option can be used to enable the mail preview in environments
+ other than development (such as staging).
- Example:
+ Defaults to `true` in development and false elsewhere.
- class MailerWithCallback < ActionMailer::Base
- after_action :a_callback
+ *Leonard Garvey*
- def welcome
- mail subject: "subject", to: ["joe@example.com"]
- end
+* Allow preview interceptors to be registered through
+ `config.action_mailer.preview_interceptors`.
- def a_callback
- mail # => returns the current mail message
- end
- end
+ See #15739.
*Yves Senn*
-* Instrument the generation of Action Mailer messages. The time it takes to
- generate a message is written to the log.
-
- *Daniel Schierbeck*
-
-* Invoke mailer defaults as procs only if they are procs, do not convert with
- `to_proc`. That an object is convertible to a proc does not mean it's meant
- to be always used as a proc.
-
- Fixes #11533.
-
- *Alex Tsukernik*
-
-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/README.rdoc b/actionmailer/README.rdoc
index e425282fa8..ceca912ada 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -102,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
})
@@ -156,7 +156,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index 9b25feaf75..01d97b7213 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -22,5 +22,5 @@ Gem::Specification.new do |s|
s.add_dependency 'actionpack', version
s.add_dependency 'actionview', version
- s.add_dependency 'mail', '~> 2.5.4'
+ s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index eb8cca9ee4..bc540aece0 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -94,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) %>
@@ -154,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>,
@@ -301,12 +301,13 @@ module ActionMailer
# end
# end
#
- # Callbacks in ActionMailer are implemented using AbstractController::Callbacks, so you
- # can define and configure callbacks in the same manner that you would use callbacks in
- # classes that inherit from ActionController::Base.
+ # Callbacks in Action Mailer are implemented using
+ # <tt>AbstractController::Callbacks</tt>, so you can define and configure
+ # callbacks in the same manner that you would use callbacks in classes that
+ # inherit from <tt>ActionController::Base</tt>.
#
# 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.
+ # rather than after_action in your Action Mailer classes so that headers are parsed properly.
#
# = Previewing emails
#
@@ -325,7 +326,7 @@ module ActionMailer
# 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"
+ # 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.
@@ -339,7 +340,7 @@ module ActionMailer
# end
# end
#
- # config.action_mailer.register_preview_interceptor :css_inline_styler
+ # config.action_mailer.preview_interceptors :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
@@ -368,8 +369,8 @@ module ActionMailer
# This is a symbol and one of <tt>:plain</tt> (will send the password in the clear), <tt>:login</tt> (will
# send password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange
# information and a cryptographic Message Digest 5 algorithm to hash important information)
- # * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server
- # and starts to use it.
+ # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts
+ # to use it. Defaults to <tt>true</tt>.
# * <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 (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>,
@@ -393,7 +394,7 @@ module ActionMailer
# implement for a custom delivery agent.
#
# * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
- # call <tt>.deliver</tt> on an mail message or on an Action Mailer method. This is on by default but can
+ # call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can
# be turned off to aid in functional testing.
#
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
@@ -648,7 +649,22 @@ module ActionMailer
# mail.attachments[0] # => Mail::Part (first attachment)
#
def attachments
- @_message.attachments
+ if @_mail_was_called
+ LateAttachmentsProxy.new(@_message.attachments)
+ else
+ @_message.attachments
+ end
+ end
+
+ class LateAttachmentsProxy < SimpleDelegator
+ def inline; _raise_error end
+ def []=(_name, _content); _raise_error end
+
+ private
+ def _raise_error
+ raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
+ "Make sure to use `attachments[]=` before calling `mail`."
+ end
end
# The main method that creates the message and renders the email templates. There are
@@ -724,11 +740,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
@@ -881,6 +897,11 @@ module ActionMailer
container.add_part(part)
end
+ # Emails do not support relative path links.
+ def self.supports_path?
+ false
+ end
+
ActiveSupport.run_load_hooks(:action_mailer, self)
end
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 eb6fb11d81..5b57c75ec3 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -6,24 +6,27 @@ module ActionMailer
class LogSubscriber < ActiveSupport::LogSubscriber
# An email was delivered.
def deliver(event)
- return unless logger.info?
- recipients = Array(event.payload[:to]).join(', ')
- info("\nSent mail to #{recipients} (#{event.duration.round(1)}ms)")
- debug(event.payload[:mail])
+ info do
+ recipients = Array(event.payload[:to]).join(', ')
+ "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)"
+ end
+
+ debug { event.payload[:mail] }
end
# An email was received.
def receive(event)
- return unless logger.info?
- info("\nReceived mail (#{event.duration.round(1)}ms)")
- debug(event.payload[:mail])
+ info { "\nReceived mail (#{event.duration.round(1)}ms)" }
+ 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")
+ debug do
+ mailer = event.payload[:mailer]
+ action = event.payload[:action]
+ "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
+ end
end
# Use the logger configured for ActionMailer::Base
diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
index 54ad9f066f..483277af04 100644
--- a/actionmailer/lib/action_mailer/mail_helper.rb
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -11,8 +11,8 @@ module ActionMailer
}.join("\n\n")
# Make list points stand on their own line
- formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
- formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
formatted
end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
index fde0e86f5b..33de1dc049 100644
--- a/actionmailer/lib/action_mailer/preview.rb
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -11,6 +11,14 @@ module ActionMailer
#
mattr_accessor :preview_path, instance_writer: false
+ # Enable or disable mailer previews through app configuration:
+ #
+ # config.action_mailer.show_previews = true
+ #
+ # Defaults to true for development environment
+ #
+ mattr_accessor :show_previews, instance_writer: false
+
# :nodoc:
mattr_accessor :preview_interceptors, instance_writer: false
self.preview_interceptors = []
@@ -20,7 +28,7 @@ module ActionMailer
interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
end
- # Register am Interceptor which will be called before mail is previewed.
+ # 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)
@@ -94,6 +102,10 @@ module ActionMailer
Base.preview_path
end
+ def show_previews #:nodoc:
+ Base.show_previews
+ end
+
def inform_preview_interceptors(message) #:nodoc:
Base.preview_interceptors.each do |interceptor|
interceptor.previewing_email(message)
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index 8d1e40297b..c62d4b5082 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -18,8 +18,9 @@ module ActionMailer
options.assets_dir ||= paths["public"].first
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
+ options.show_previews = Rails.env.development? if options.show_previews.nil?
- if Rails.env.development?
+ if options.show_previews
options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
end
@@ -29,13 +30,21 @@ module ActionMailer
ActiveSupport.on_load(:action_mailer) do
include AbstractController::UrlFor
- extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
+ extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
include app.routes.mounted_helpers
register_interceptors(options.delete(:interceptors))
+ register_preview_interceptors(options.delete(:preview_interceptors))
register_observers(options.delete(:observers))
options.each { |k,v| send("#{k}=", v) }
+
+ if options.show_previews
+ app.routes.append do
+ get '/rails/mailers' => "rails/mailers#index"
+ get '/rails/mailers/*path' => "rails/mailers#preview"
+ end
+ end
end
end
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index 207f949fe2..a5442c0316 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -20,6 +20,7 @@ module ActionMailer
class_attribute :_mailer_class
setup :initialize_test_deliveries
setup :set_expected_mail
+ teardown :restore_test_deliveries
end
module ClassMethods
@@ -54,8 +55,15 @@ module ActionMailer
protected
def initialize_test_deliveries
+ @old_delivery_method = ActionMailer::Base.delivery_method
+ @old_perform_deliveries = ActionMailer::Base.perform_deliveries
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
+ end
+
+ def restore_test_deliveries
+ ActionMailer::Base.delivery_method = @old_delivery_method
+ ActionMailer::Base.perform_deliveries = @old_perform_deliveries
ActionMailer::Base.deliveries.clear
end
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index 6452bf616c..06da0dd27e 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -1,4 +1,6 @@
module ActionMailer
+ # Provides helper methods for testing Action Mailer, including #assert_emails
+ # and #assert_no_emails
module TestHelper
# Asserts that the number of emails sent matches the given number.
#
diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 46eb763c26..06f80a8fdc 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -1,11 +1,9 @@
+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 Action Mailer as a
+ # <tt>Gem::Version</tt>.
def self.version
- Gem::Version.new "4.1.0.beta1"
- 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 93d16f491d..98d266bd73 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -23,34 +23,12 @@ 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 }
-
FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__))
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-class MockSMTP
- def self.deliveries
- @@deliveries
- end
-
- def initialize
- @@deliveries = []
- end
-
- def sendmail(mail, from, to)
- @@deliveries << [mail, from, to]
- end
-
- def start(*args)
- yield self
- end
-end
-
-class Net::SMTP
- def self.new(*args)
- MockSMTP.new
+class Rails
+ def self.root
+ File.expand_path('../', File.dirname(__FILE__))
end
end
@@ -71,3 +49,5 @@ end
def jruby_skip(message = '')
skip message if defined?(JRUBY_VERSION)
end
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb
index 00f1348a53..9ba67c2842 100644
--- a/actionmailer/test/asset_host_test.rb
+++ b/actionmailer/test/asset_host_test.rb
@@ -31,25 +31,10 @@ class AssetHostTest < ActiveSupport::TestCase
def test_asset_host_as_one_argument_proc
AssetHostMailer.config.asset_host = Proc.new { |source|
if source.starts_with?('/images')
- "http://images.example.com"
- else
- "http://assets.example.com"
+ 'http://images.example.com'
end
}
mail = AssetHostMailer.email_with_asset
assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
-
- def test_asset_host_as_two_argument_proc
- ActionController::Base.config.asset_host = Proc.new {|source,request|
- if request && request.ssl?
- "https://www.example.com"
- else
- "http://www.example.com"
- end
- }
- mail = nil
- assert_nothing_raised { mail = AssetHostMailer.email_with_asset }
- assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
- end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 02707d0b5f..fd5f4e2831 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -10,9 +10,18 @@ require 'mailers/proc_mailer'
require 'mailers/asset_mailer'
class BaseTest < ActiveSupport::TestCase
- def teardown
- ActionMailer::Base.asset_host = nil
- ActionMailer::Base.assets_dir = nil
+ setup do
+ @original_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ @original_asset_host = ActionMailer::Base.asset_host
+ @original_assets_dir = ActionMailer::Base.assets_dir
+ end
+
+ teardown do
+ ActionMailer::Base.asset_host = @original_asset_host
+ ActionMailer::Base.assets_dir = @original_assets_dir
+ BaseMailer.deliveries.clear
+ ActionMailer::Base.delivery_method = @original_delivery_method
end
test "method call to mail does not raise error" do
@@ -103,6 +112,7 @@ class BaseTest < ActiveSupport::TestCase
test "attachment gets content type from filename" do
email = BaseMailer.attachment_with_content
assert_equal('invoice.pdf', email.attachments[0].filename)
+ assert_equal('application/pdf', email.attachments[0].mime_type)
end
test "attachment with hash" do
@@ -200,25 +210,68 @@ class BaseTest < ActiveSupport::TestCase
end
test "subject gets default from I18n" do
- BaseMailer.default subject: nil
- email = BaseMailer.welcome(subject: nil)
- assert_equal "Welcome", email.subject
+ with_default BaseMailer, subject: nil do
+ email = BaseMailer.welcome(subject: nil)
+ assert_equal "Welcome", email.subject
- I18n.backend.store_translations('en', base_mailer: {welcome: {subject: "New Subject!"}})
- email = BaseMailer.welcome(subject: nil)
- assert_equal "New Subject!", email.subject
+ with_translation 'en', base_mailer: {welcome: {subject: "New Subject!"}} do
+ email = BaseMailer.welcome(subject: nil)
+ assert_equal "New Subject!", email.subject
+ end
+ end
end
test 'default subject can have interpolations' do
- I18n.backend.store_translations('en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}})
- email = BaseMailer.with_subject_interpolations
- assert_equal 'Will the real Slim Shady please stand up?', email.subject
+ with_translation 'en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}} do
+ email = BaseMailer.with_subject_interpolations
+ assert_equal 'Will the real Slim Shady please stand up?', email.subject
+ end
end
test "translations are scoped properly" do
- I18n.backend.store_translations('en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}})
- email = BaseMailer.email_with_translations
- assert_equal 'Hello lifo!', email.body.encoded
+ with_translation 'en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}} do
+ email = BaseMailer.email_with_translations
+ assert_equal 'Hello lifo!', email.body.encoded
+ end
+ end
+
+ test "adding attachments after mail was called raises exception" do
+ class LateAttachmentMailer < ActionMailer::Base
+ def welcome
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+ attachments['invoice.pdf'] = 'This is test File content'
+ end
+ end
+
+ e = assert_raises(RuntimeError) { LateAttachmentMailer.welcome }
+ assert_match(/Can't add attachments after `mail` was called./, e.message)
+ end
+
+ test "adding inline attachments after mail was called raises exception" do
+ class LateInlineAttachmentMailer < ActionMailer::Base
+ def welcome
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+ attachments.inline['invoice.pdf'] = 'This is test File content'
+ end
+ end
+
+ e = assert_raises(RuntimeError) { LateInlineAttachmentMailer.welcome }
+ assert_match(/Can't add attachments after `mail` was called./, e.message)
+ end
+
+ test "accessing attachments works after mail was called" do
+ class LateAttachmentAccessorMailer < ActionMailer::Base
+ def welcome
+ attachments['invoice.pdf'] = 'This is test File content'
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+
+ unless attachments.map(&:filename) == ["invoice.pdf"]
+ raise Minitest::Assertion, "Should allow access to attachments"
+ end
+ end
+ end
+
+ assert_nothing_raised { LateAttachmentAccessorMailer.welcome }
end
# Implicit multipart
@@ -406,22 +459,18 @@ class BaseTest < ActiveSupport::TestCase
end
test "calling just the action should return the generated mail object" do
- BaseMailer.deliveries.clear
email = BaseMailer.welcome
assert_equal(0, BaseMailer.deliveries.length)
assert_equal('The first email on new API!', email.subject)
end
test "calling deliver on the action should deliver the mail object" do
- BaseMailer.deliveries.clear
BaseMailer.expects(:deliver_mail).once
mail = BaseMailer.welcome.deliver
assert_equal 'The first email on new API!', mail.subject
end
test "calling deliver on the action should increment the deliveries collection if using the test mailer" do
- BaseMailer.delivery_method = :test
- BaseMailer.deliveries.clear
BaseMailer.welcome.deliver
assert_equal(1, BaseMailer.deliveries.length)
end
@@ -441,7 +490,6 @@ class BaseTest < ActiveSupport::TestCase
end
test "should raise if missing template in implicit render" do
- BaseMailer.deliveries.clear
assert_raises ActionView::MissingTemplate do
BaseMailer.implicit_different_template('missing_template').deliver
end
@@ -478,18 +526,17 @@ class BaseTest < ActiveSupport::TestCase
end
test "assets tags should use a Mailer's asset_host settings when available" do
- begin
- ActionMailer::Base.config.asset_host = "http://global.com"
- ActionMailer::Base.config.assets_dir = "global/"
+ ActionMailer::Base.config.asset_host = "http://global.com"
+ ActionMailer::Base.config.assets_dir = "global/"
- AssetMailer.asset_host = "http://local.com"
+ TempAssetMailer = Class.new(AssetMailer) do
+ self.mailer_name = "asset_mailer"
+ self.asset_host = "http://local.com"
+ end
- mail = AssetMailer.welcome
+ mail = TempAssetMailer.welcome
- assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip)
- ensure
- AssetMailer.asset_host = ActionMailer::Base.config.asset_host
- end
+ assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip)
end
test 'the view is not rendered when mail was never called' do
@@ -517,32 +564,40 @@ class BaseTest < ActiveSupport::TestCase
end
test "you can register an observer to the mail object that gets informed on email delivery" do
- ActionMailer::Base.register_observer(MyObserver)
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_observer(MyObserver)
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver
+ end
end
test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do
- ActionMailer::Base.register_observer("BaseTest::MyObserver")
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_observer("BaseTest::MyObserver")
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver
+ end
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
+ mail_side_effects do
+ ActionMailer::Base.register_observer(:"base_test/my_observer")
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver
+ end
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
- MyObserver.expects(:delivered_email).with(mail)
- MySecondObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ MySecondObserver.expects(:delivered_email).with(mail)
+ mail.deliver
+ end
end
class MyInterceptor
@@ -555,72 +610,41 @@ class BaseTest < ActiveSupport::TestCase
def self.previewing_email(mail); end
end
- class BaseMailerPreview < ActionMailer::Preview
- def welcome
- BaseMailer.welcome
- end
- end
-
test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do
- ActionMailer::Base.register_interceptor(MyInterceptor)
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_interceptor(MyInterceptor)
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver
+ end
end
test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do
- ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver
+ end
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
+ mail_side_effects do
+ ActionMailer::Base.register_interceptor(:"base_test/my_interceptor")
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver
+ end
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
- MyInterceptor.expects(:delivering_email).with(mail)
- MySecondInterceptor.expects(:delivering_email).with(mail)
- 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.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.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.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.stubs(:welcome).returns(mail)
- MyInterceptor.expects(:previewing_email).with(mail)
- MySecondInterceptor.expects(:previewing_email).with(mail)
- BaseMailerPreview.call(:welcome)
+ mail_side_effects do
+ ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ MySecondInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver
+ end
end
test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do
@@ -769,4 +793,77 @@ class BaseTest < ActiveSupport::TestCase
ensure
klass.default_params = old
end
+
+ # A simple hack to restore the observers and interceptors for Mail, as it
+ # does not have an unregister API yet.
+ def mail_side_effects
+ old_observers = Mail.class_variable_get(:@@delivery_notification_observers)
+ old_delivery_interceptors = Mail.class_variable_get(:@@delivery_interceptors)
+ yield
+ ensure
+ Mail.class_variable_set(:@@delivery_notification_observers, old_observers)
+ Mail.class_variable_set(:@@delivery_interceptors, old_delivery_interceptors)
+ end
+
+ def with_translation(locale, data)
+ I18n.backend.store_translations(locale, data)
+ yield
+ ensure
+ I18n.backend.reload!
+ end
+end
+
+class BasePreviewInterceptorsTest < ActiveSupport::TestCase
+ teardown do
+ ActionMailer::Base.preview_interceptors.clear
+ end
+
+ class BaseMailerPreview < ActionMailer::Preview
+ def welcome
+ BaseMailer.welcome
+ end
+ end
+
+ class MyInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ class MySecondInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ 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("BasePreviewInterceptorsTest::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_preview_interceptors_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("BasePreviewInterceptorsTest::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
end
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 609903620b..a76ac6d295 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -47,12 +47,12 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
end
class CustomDeliveryMethodsTest < ActiveSupport::TestCase
- def setup
+ setup do
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.add_delivery_method :custom, MyCustomDelivery
end
- def teardown
+ teardown do
ActionMailer::Base.delivery_method = @old_delivery_method
new = ActionMailer::Base.delivery_methods.dup
new.delete(:custom)
@@ -93,23 +93,22 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
end
- def setup
- ActionMailer::Base.delivery_method = :smtp
+ setup do
+ @old_delivery_method = DeliveryMailer.delivery_method
end
- def teardown
- DeliveryMailer.delivery_method = :smtp
- DeliveryMailer.perform_deliveries = true
- DeliveryMailer.raise_delivery_errors = true
+ teardown do
+ DeliveryMailer.delivery_method = @old_delivery_method
+ DeliveryMailer.deliveries.clear
end
test "ActionMailer should be told when Mail gets delivered" do
- DeliveryMailer.deliveries.clear
DeliveryMailer.expects(:deliver_mail).once
DeliveryMailer.welcome.deliver
end
test "delivery method can be customized per instance" do
+ Mail::SMTP.any_instance.expects(:deliver!)
email = DeliveryMailer.welcome.deliver
assert_instance_of Mail::SMTP, email.delivery_method
email = DeliveryMailer.welcome(delivery_method: :test).deliver
@@ -119,7 +118,6 @@ class MailDeliveryTest < ActiveSupport::TestCase
test "delivery method can be customized in subclasses not changing the parent" do
DeliveryMailer.delivery_method = :test
assert_equal :smtp, ActionMailer::Base.delivery_method
- $BREAK = true
email = DeliveryMailer.welcome.deliver
assert_instance_of Mail::TestMailer, email.delivery_method
end
@@ -176,22 +174,29 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
test "does not perform deliveries if requested" do
- DeliveryMailer.perform_deliveries = false
- DeliveryMailer.deliveries.clear
- Mail::Message.any_instance.expects(:deliver!).never
- DeliveryMailer.welcome.deliver
+ old_perform_deliveries = DeliveryMailer.perform_deliveries
+ begin
+ DeliveryMailer.perform_deliveries = false
+ Mail::Message.any_instance.expects(:deliver!).never
+ DeliveryMailer.welcome.deliver
+ ensure
+ DeliveryMailer.perform_deliveries = old_perform_deliveries
+ end
end
test "does not append the deliveries collection if told not to perform the delivery" do
- DeliveryMailer.perform_deliveries = false
- DeliveryMailer.deliveries.clear
- DeliveryMailer.welcome.deliver
- assert_equal(0, DeliveryMailer.deliveries.length)
+ old_perform_deliveries = DeliveryMailer.perform_deliveries
+ begin
+ DeliveryMailer.perform_deliveries = false
+ DeliveryMailer.welcome.deliver
+ assert_equal [], DeliveryMailer.deliveries
+ ensure
+ DeliveryMailer.perform_deliveries = old_perform_deliveries
+ end
end
test "raise errors on bogus deliveries" do
DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.deliveries.clear
assert_raise RuntimeError do
DeliveryMailer.welcome.deliver
end
@@ -199,27 +204,34 @@ class MailDeliveryTest < ActiveSupport::TestCase
test "does not increment the deliveries collection on error" do
DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.deliveries.clear
assert_raise RuntimeError do
DeliveryMailer.welcome.deliver
end
- assert_equal(0, DeliveryMailer.deliveries.length)
+ assert_equal [], DeliveryMailer.deliveries
end
test "does not raise errors on bogus deliveries if set" do
- DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.raise_delivery_errors = false
- assert_nothing_raised do
- DeliveryMailer.welcome.deliver
+ old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors
+ begin
+ DeliveryMailer.delivery_method = BogusDelivery
+ DeliveryMailer.raise_delivery_errors = false
+ assert_nothing_raised do
+ DeliveryMailer.welcome.deliver
+ end
+ ensure
+ DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
end
end
test "does not increment the deliveries collection on bogus deliveries" do
- DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.raise_delivery_errors = false
- DeliveryMailer.deliveries.clear
- DeliveryMailer.welcome.deliver
- assert_equal(0, DeliveryMailer.deliveries.length)
+ old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors
+ begin
+ DeliveryMailer.delivery_method = BogusDelivery
+ DeliveryMailer.raise_delivery_errors = false
+ DeliveryMailer.welcome.deliver
+ assert_equal [], DeliveryMailer.deliveries
+ ensure
+ DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
+ end
end
-
end
diff --git a/actionmailer/test/fixtures/test_helper_mailer/welcome b/actionmailer/test/fixtures/test_helper_mailer/welcome
new file mode 100644
index 0000000000..61ce70d578
--- /dev/null
+++ b/actionmailer/test/fixtures/test_helper_mailer/welcome
@@ -0,0 +1 @@
+Welcome! \ 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 14a1b11b6d..ee36b89dd6 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -10,15 +10,15 @@ class I18nTestMailer < ActionMailer::Base
def mail_with_i18n_subject(recipient)
@recipient = recipient
I18n.locale = :de
- mail(to: recipient, subject: "#{I18n.t :email_subject} #{recipient}",
+ mail(to: recipient, subject: I18n.t(:email_subject),
from: "system@loudthinking.com", date: Time.local(2004, 12, 12))
end
end
class TestController < ActionController::Base
def send_mail
- I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver
- render text: 'Mail sent'
+ email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver
+ render text: "Mail sent - Subject: #{email.subject}"
end
end
@@ -28,20 +28,43 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
get ':controller(/:action(/:id))'
end
- def app
- Routes
+ class RoutedRackApp
+ attr_reader :routes
+
+ def initialize(routes, &blk)
+ @routes = routes
+ @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes)
+ end
+
+ def call(env)
+ @stack.call(env)
+ end
end
- def setup
- I18n.backend.store_translations('de', email_subject: '[Signed up] Welcome')
+ APP = RoutedRackApp.new(Routes)
+
+ def app
+ APP
end
- def teardown
- I18n.locale = :en
+ teardown do
+ I18n.locale = I18n.default_locale
end
def test_send_mail
- get '/test/send_mail'
- assert_equal "Mail sent", @response.body
+ Mail::SMTP.any_instance.expects(:deliver!)
+ with_translation 'de', email_subject: '[Anmeldung] Willkommen' do
+ get '/test/send_mail'
+ assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body
+ end
+ end
+
+ protected
+
+ def with_translation(locale, data)
+ I18n.backend.store_translations(locale, data)
+ yield
+ ensure
+ I18n.backend.reload!
end
end
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index 5f0bee88fd..e7a73d6c8e 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -1,7 +1,7 @@
-require "abstract_unit"
+require 'abstract_unit'
require 'mailers/base_mailer'
-require "active_support/log_subscriber/test_helper"
-require "action_mailer/log_subscriber"
+require 'active_support/log_subscriber/test_helper'
+require 'action_mailer/log_subscriber'
class AMLogSubscriberTest < ActionMailer::TestCase
include ActiveSupport::LogSubscriber::TestHelper
@@ -31,6 +31,8 @@ class AMLogSubscriberTest < ActionMailer::TestCase
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)
+ ensure
+ BaseMailer.deliveries.clear
end
def test_receive_is_notified
diff --git a/actionmailer/test/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb
index 7f959282cb..166dd096d4 100644
--- a/actionmailer/test/mail_layout_test.rb
+++ b/actionmailer/test/mail_layout_test.rb
@@ -44,16 +44,6 @@ class ExplicitLayoutMailer < ActionMailer::Base
end
class LayoutMailerTest < ActiveSupport::TestCase
- def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
- end
-
- def teardown
- restore_delivery_method
- end
-
def test_should_pickup_default_layout
mail = AutoLayoutMailer.hello
assert_equal "Hello from layout Inside", mail.body.to_s.strip
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index 7c7f0b6fdc..1ff08a3b6e 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
require 'abstract_unit'
class TestHelperMailer < ActionMailer::Base
@@ -36,6 +37,14 @@ class TestHelperMailerTest < ActionMailer::TestCase
assert_equal "UTF-8", charset
end
+ def test_encode
+ assert_equal '=?UTF-8?Q?This_is_=E3=81=82_string?=', encode('This is あ string')
+ end
+
+ def test_read_fixture
+ assert_equal ['Welcome!'], read_fixture('welcome')
+ end
+
def test_assert_emails
assert_nothing_raised do
assert_emails 1 do
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 342f670e78..7c460fbaef 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,515 +1,251 @@
-* Add new config option `config.action_dispatch.cookies_serializer` for
- specifying a serializer for the signed and encrypted cookie jars.
+* Remove deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
+ in favor of `AbstractController::Helpers::MissingHelperError`.
- The possible values are:
-
- * `:json` - serialize cookie values with `JSON`
- * `:marshal` - serialize cookie values with `Marshal`
- * `:hybrid` - transparently migrate existing `Marshal` cookie values to `JSON`
-
- For new apps `:json` option is added by default and `:marshal` is used
- when no option is specified to maintain backwards compatibility.
-
- *Łukasz Sarnacki*, *Matt Aimonetti*, *Guillermo Iguaran*, *Godfrey Chan*, *Rafael Mendonça França*
-
-* `FlashHash` now behaves like a `HashWithIndifferentAccess`.
-
- *Guillermo Iguaran*
-
-* Set the `:shallow_path` scope option as each scope is generated rather than
- waiting until the `shallow` option is set. Also make the behavior of the
- `:shallow` resource option consistent with the behavior of the `shallow` method.
-
- Fixes #12498.
-
- *Andrew White*, *Aleksi Aalto*
-
-* Properly require `action_view` in `AbstractController::Rendering` to prevent
- uninitialized constant error for `ENCODING_FLAG`.
-
- *Philipe Fatio*
-
-* Do not discard query parameters that form a hash with the same root key as
- the `wrapper_key` for a request using `wrap_parameters`.
-
- *Josh Jordan*
-
-* Ensure that `request.filtered_parameters` is reset between calls to `process`
- in `ActionController::TestCase`.
-
- Fixes #13803.
-
- *Andrew White*
-
-* Fix `rake routes` error when `Rails::Engine` with empty routes is mounted.
-
- Fixes #13810.
-
- *Maurizio De Santis*
-
-* Log which keys were affected by deep munge.
-
- Deep munge solves CVE-2013-0155 security vulnerability, but its
- behaviour is definately confusing, so now at least information
- about for which keys values were set to nil is visible in logs.
-
- *Łukasz Sarnacki*
-
-* Automatically convert dashes to underscores for shorthand routes, e.g:
-
- get '/our-work/latest'
-
- When running `rake routes` you will get the following output:
-
- Prefix Verb URI Pattern Controller#Action
- our_work_latest GET /our-work/latest(.:format) our_work#latest
-
- *Mikko Johansson*
-
-* Automatically convert dashes to underscores for url helpers, e.g:
-
- get '/contact-us' => 'pages#contact'
- get '/about-us' => 'pages#about_us'
-
- When running `rake routes` you will get the following output:
-
- Prefix Verb URI Pattern Controller#Action
- contact_us GET /contact-us(.:format) pages#contact
- about_us GET /about-us(.:format) pages#about_us
-
- *Amr Tamimi*
-
-* Fix stream closing when sending file with `ActionController::Live` included.
-
- Fixes #12381
-
- *Alessandro Diaferia*
-
-* Allow an absolute controller path inside a module scope. Fixes #12777.
-
- Example:
-
- namespace :foo do
- # will route to BarController without the namespace.
- get '/special', to: '/bar#index'
- end
-
-
-* Unique the segment keys array for non-optimized url helpers
-
- In Rails 3.2 you only needed pass an argument for dynamic segment once so
- unique the segment keys array to match the number of args. Since the number
- of args is less than required parts the non-optimized code path is selected.
- This means to benefit from optimized url generation the arg needs to be
- specified as many times as it appears in the path.
-
- Fixes #12808.
-
- *Andrew White*
-
-* Show full route constraints in error message.
-
- When an optimized helper fails to generate, show the full route constraints
- in the error message. Previously it would only show the contraints that were
- required as part of the path.
-
- Fixes #13592.
-
- *Andrew White*
-
-* Use a custom route visitor for optimized url generation. Fixes #13349.
-
- *Andrew White*
-
-* Allow engine root relative redirects using an empty string.
-
- Example:
-
- # application routes.rb
- mount BlogEngine => '/blog'
-
- # engine routes.rb
- get '/welcome' => redirect('')
-
- This now redirects to the path `/blog`, whereas before it would redirect
- to the application root path. In the case of a path redirect or a custom
- redirect if the path returned contains a host then the path is treated as
- absolute. Similarly for option redirects, if the options hash returned
- contains a `:host` or `:domain` key then the path is treated as absolute.
-
- Fixes #7977.
-
- *Andrew White*
-
-* Fix `Encoding::CompatibilityError` when public path is UTF-8
-
- In #5337 we forced the path encoding to ASCII-8BIT to prevent static file handling
- from blowing up before an application has had chance to deal with possibly invalid
- urls. However this has a negative side effect of making it an incompatible encoding
- if the application's public path has UTF-8 characters in it.
-
- To work around the problem we check to see if the path has a valid encoding once
- it has been unescaped. If it is not valid then we can return early since it will
- not match any file anyway.
-
- Fixes #13518.
-
- *Andrew White*
-
-* `ActionController::Parameters#permit!` permits hashes in array values.
-
- *Xavier Noria*
-
-* Converts hashes in arrays of unfiltered params to unpermitted params.
-
- Fixes #13382.
-
- *Xavier Noria*
-
-* New config option to opt out of params "deep munging" that was used to
- address security vulnerability CVE-2013-0155. In your app config:
-
- config.action_dispatch.perform_deep_munge = false
-
- Take care to understand the security risk involved before disabling this.
- [Read more.](https://groups.google.com/forum/#!topic/rubyonrails-security/t1WFuuQyavI)
-
- *Bernard Potocki*
-
-* `rake routes` shows routes defined under assets prefix.
-
- *Ryunosuke SATO*
-
-* Extend cross-site request forgery (CSRF) protection to GET requests with
- JavaScript responses, protecting apps from cross-origin `<script>` tags.
-
- *Jeremy Kemper*
-
-* Fix generating a path for engine inside a resources block.
-
- Fixes #8533.
-
- *Piotr Sarnacki*
-
-* Add `Mime::Type.register "text/vcard", :vcf` to the default list of mime types.
-
- *DHH*
-
-* Remove deprecated `ActionController::RecordIdentifier`, use
- `ActionView::RecordIdentifier` instead.
-
- *kennyj*
-
-* Fix regression when using `ActionView::Helpers::TranslationHelper#translate` with
- `options[:raise]`.
+ *Yves Senn*
- This regression was introduced at ec16ba75a5493b9da972eea08bae630eba35b62f.
+* Fix `assert_template` not being able to assert that no files were rendered.
- *Shota Fukumori (sora_h)*
+ *Guo Xiang Tan*
-* Introducing Variants
+* Extract source code for the entire exception stack trace for
+ better debugging and diagnosis.
- We often want to render different html/json/xml templates for phones,
- tablets, and desktop browsers. Variants make it easy.
+ *Ryan Dao*
- The request variant is a specialization of the request format, like `:tablet`,
- `:phone`, or `:desktop`.
+* Allows ActionDispatch::Request::LOCALHOST to match any IPv4 127.0.0.0/8
+ loopback address.
- You can set the variant in a `before_action`:
+ *Earl St Sauver*, *Sven Riedel*
- request.variant = :tablet if request.user_agent =~ /iPad/
+* Preserve original path in `ShowExceptions` middleware by stashing it as
+ `env["action_dispatch.original_path"]`
- Respond to variants in the action just like you respond to formats:
+ `ActionDispatch::ShowExceptions` overwrites `PATH_INFO` with the status code
+ for the exception defined in `ExceptionWrapper`, so the path
+ the user was visiting when an exception occurred was not previously
+ available to any custom exceptions_app. The original `PATH_INFO` is now
+ stashed in `env["action_dispatch.original_path"]`.
- 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
+ *Grey Baker*
- Provide separate templates for each format and variant:
+* Use `String#bytesize` instead of `String#size` when checking for cookie
+ overflow.
- app/views/projects/show.html.erb
- app/views/projects/show.html+tablet.erb
- app/views/projects/show.html+phone.erb
+ *Agis Anastasopoulos*
- You can also simplify the variants definition using the inline syntax:
+* `render nothing: true` or rendering a `nil` body no longer add a single
+ space to the response body.
- respond_to do |format|
- format.js { render "trash" }
- format.html.phone { redirect_to progress_path }
- format.html.none { render "trash" }
- end
+ The old behavior was added as a workaround for a bug in an early version of
+ Safari, where the HTTP headers are not returned correctly if the response
+ body has a 0-length. This is been fixed since and the workaround is no
+ longer necessary.
- Variants also support common `any`/`all` block that formats have.
+ Use `render body: ' '` if the old behavior is desired.
- It works for both inline:
+ See #14883 for details.
- respond_to do |format|
- format.html.any { render text: "any" }
- format.html.phone { render text: "phone" }
- end
+ *Godfrey Chan*
- and block syntax:
+* Prepend a JS comment to JSONP callbacks. Addresses CVE-2014-4671
+ ("Rosetta Flash")
- respond_to do |format|
- format.html do |variant|
- variant.any(:tablet, :phablet){ render text: "any" }
- variant.phone { render text: "phone" }
- end
- end
+ *Greg Campbell*
- *Łukasz Strzałkowski*
+* Because URI paths may contain non US-ASCII characters we need to force
+ the encoding of any unescaped URIs to UTF-8 if they are US-ASCII.
+ This essentially replicates the functionality of the monkey patch to
+ URI.parser.unescape in active_support/core_ext/uri.rb.
-* Fix render of localized templates without an explicit format using wrong
- content header and not passing correct formats to template due to the
- introduction of the `NullType` for mimes.
+ Fixes #16104.
- Templates like `hello.it.erb` were subject to this issue.
+ *Karl Entwistle*
- Fixes #13064.
+* Generate shallow paths for all children of shallow resources.
- *Angelo Capilleri*, *Carlos Antonio da Silva*
+ Fixes #15783.
-* Try to escape each part of a url correctly when using a redirect route.
+ *Seb Jacobs*
- Fixes #13110.
+* JSONP responses are now rendered with the `text/javascript` content type
+ when rendering through a `respond_to` block.
- *Andrew White*
+ Fixes #15081.
-* Better error message for typos in assert_response argument.
+ *Lucas Mazza*
- When the response type argument to `assert_response` is not a known
- response type, `assert_response` now throws an ArgumentError with a clear
- message. This is intended to help debug typos in the response type.
+* Add `config.action_controller.always_permitted_parameters` to configure which
+ parameters are permitted globally. The default value of this configuration is
+ `['controller', 'action']`.
- *Victor Costan*
+ *Gary S. Weaver*, *Rafael Chacon*
-* Fix formatting for `rake routes` when a section is shorter than a header.
+* Fix env['PATH_INFO'] missing leading slash when a rack app mounted at '/'.
- *Sıtkı Bağdat*
+ Fixes #15511.
-* Take a hash with options inside array in `#url_for`.
+ *Larry Lv*
- Example:
+* ActionController::Parameters#require now accepts `false` values.
- url_for [:new, :admin, :post, { param: 'value' }]
- # => http://example.com/admin/posts/new?param=value
+ Fixes #15685.
- *Andrey Ognevsky*
+ *Sergio Romano*
-* Add `session#fetch` method
+* With authorization header `Authorization: Token token=`, `authenticate` now
+ recognize token as nil, instead of "token".
- fetch behaves like [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch).
- It returns a value from the hash for the given key.
- If the key can’t be found, there are several options:
+ Fixes #14846.
- * With no other arguments, it will raise an KeyError exception.
- * If a default value is given, then that will be returned.
- * If the optional code block is specified, then that will be run and its result returned.
+ *Larry Lv*
- *Damien Mathieu*
+* Ensure the controller is always notified as soon as the client disconnects
+ during live streaming, even when the controller is blocked on a write.
-* Don't let strong parameters mutate the given hash via `fetch`
+ *Nicholas Jakobsen*, *Matthew Draper*
- Create a new instance if the given parameter is a `Hash` instead of
- passing it to the `convert_hashes_to_parameters` method since it is
- overriding its default value.
+* Routes specifying 'to:' must be a string that contains a "#" or a rack
+ application. Use of a symbol should be replaced with `action: symbol`.
+ Use of a string without a "#" should be replaced with `controller: string`.
- *Brendon Murphy*, *Doug Cole*
+ *Aaron Patterson*
-* Add `params` option to `button_to` form helper, which renders the given hash
- as hidden form fields.
+* Fix URL generation with `:trailing_slash` such that it does not add
+ a trailing slash after `.:format`
- *Andy Waite*
+ *Dan Langevin*
-* Make assets helpers work in the controllers like it works in the views.
+* Build full URI as string when processing path in integration tests for
+ performance reasons.
- Example:
+ *Guo Xiang Tan*
- # config/application.rb
- config.asset_host = 'http://mycdn.com'
+* Fix `'Stack level too deep'` when rendering `head :ok` in an action method
+ called 'status' in a controller.
- ActionController::Base.helpers.asset_path('fallback.png')
- # => http://mycdn.com/assets/fallback.png
+ Fixes #13905.
- Fixes #10051.
+ *Christiaan Van den Poel*
- *Tima Maslyuchenko*
+* Add MKCALENDAR HTTP method (RFC 4791).
-* Respect `SCRIPT_NAME` when using `redirect` with a relative path
+ *Sergey Karpesh*
- Example:
+* Instrument fragment cache metrics.
- # application routes.rb
- mount BlogEngine => '/blog'
+ Adds `:controller`: and `:action` keys to the instrumentation payload
+ for the `*_fragment.action_controller` notifications. This allows tracking
+ e.g. the fragment cache hit rates for each controller action.
- # engine routes.rb
- get '/admin' => redirect('admin/dashboard')
+ *Daniel Schierbeck*
- This now redirects to the path `/blog/admin/dashboard`, whereas before it would've
- generated an invalid url because there would be no slash between the host name and
- the path. It also allows redirects to work where the application is deployed to a
- subdirectory of a website.
+* Always use the provided port if the protocol is relative.
- Fixes #7977.
+ Fixes #15043.
- *Andrew White*
+ *Guilherme Cavalcanti*, *Andrew White*
-* Fixing repond_with working directly on the options hash
- This fixes an issue where the respond_with worked directly with the given
- options hash, so that if a user relied on it after calling respond_with,
- the hash wouldn't be the same.
+* Moved `params[request_forgery_protection_token]` into its own method
+ and improved tests.
- Fixes #12029.
+ Fixes #11316.
- *bluehotdog*
+ *Tom Kadwill*
-* Fix `ActionDispatch::RemoteIp::GetIp#calculate_ip` to only check for spoofing
- attacks if both `HTTP_CLIENT_IP` and `HTTP_X_FORWARDED_FOR` are set.
+* Added verification of route constraints given as a Proc or an object responding
+ to `:matches?`. Previously, when given an non-complying object, it would just
+ silently fail to enforce the constraint. It will now raise an `ArgumentError`
+ when setting up the routes.
- Fixes #10844.
+ *Xavier Defrang*
- *Tamir Duberstein*
+* Properly treat the entire IPv6 User Local Address space as private for
+ purposes of remote IP detection. Also handle uppercase private IPv6
+ addresses.
-* Strong parameters should permit nested number as key.
+ Fixes #12638.
- Fixes #12293.
+ *Caleb Spare*
- *kennyj*
+* Fixed an issue with migrating legacy json cookies.
-* Fix regex used to detect URI schemes in `redirect_to` to be consistent with
- RFC 3986.
+ 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.
- *Derek Prior*
+ 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.
-* Fix incorrect `assert_redirected_to` failure message for protocol-relative
- URLs.
+ Fixes #14774.
- *Derek Prior*
+ *Godfrey Chan*
-* Fix an issue where router can't recognize downcased url encoding path.
+* Make URL escaping more consistent:
- Fixes #12269.
+ 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
- *kennyj*
+ 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.
-* Fix custom flash type definition. Misusage of the `_flash_types` class variable
- caused an error when reloading controllers with custom flash types.
+ Fixes #14629, #14636 and #14070.
- Fixes #12057.
+ *Andrew White*, *Edho Arief*
- *Ricardo de Cillo*
+* Add alias `ActionDispatch::Http::UploadedFile#to_io` to
+ `ActionDispatch::Http::UploadedFile#tempfile`.
-* Do not break params filtering on `nil` values.
+ *Tim Linquist*
- Fixes #12149.
+* Returns null type format when format is not know and controller is using `any`
+ format block.
- *Vasiliy Ermolovich*
+ Fixes #14462.
-* Development mode exceptions are rendered in text format in case of XHR request.
+ *Rafael Mendonça França*
- *Kir Shatrov*
+* Improve routing error page with fuzzy matching search.
-* Fix an issue where :if and :unless controller action procs were being run
- before checking for the correct action in the :only and :unless options.
+ *Winston*
- Fixes #11799.
+* Only make deeply nested routes shallow when parent is shallow.
- *Nicholas Jakobsen*
+ Fixes #14684.
-* Fix an issue where `assert_dom_equal` and `assert_dom_not_equal` were
- ignoring the passed failure message argument.
+ *Andrew White*, *James Coglan*
- Fixes #11751.
+* Append link to bad code to backtrace when exception is `SyntaxError`.
- *Ryan McGeary*
+ *Boris Kuznetsov*
-* Allow REMOTE_ADDR, HTTP_HOST and HTTP_USER_AGENT to be overridden from
- the environment passed into `ActionDispatch::TestRequest.new`.
+* Swapped the parameters of assert_equal in `assert_select` so that the
+ proper values were printed correctly.
- Fixes #11590.
+ Fixes #14422.
- *Andrew White*
-
-* Fix an issue where Journey was failing to clear the named routes hash when the
- routes were reloaded and since it doesn't overwrite existing routes then if a
- route changed but wasn't renamed it kept the old definition. This was being
- masked by the optimised url helpers so it only became apparent when passing an
- options hash to the url helper.
-
- *Andrew White*
+ *Vishal Lal*
-* Skip routes pointing to a redirect or mounted application when generating urls
- using an options hash as they aren't relevant and generate incorrect urls.
+* 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 #8018.
+ Fixes #14388.
*Andrew White*
-* Move `MissingHelperError` out of the `ClassMethods` module.
-
- *Yves Senn*
-
-* Fix an issue where rails raise exception about missing helper where it
- should throw `LoadError`. When helper file exists and only loaded file from
- this helper does not exist rails should throw LoadError instead of
- `MissingHelperError`.
-
- *Piotr Niełacny*
-
-* Fix `ActionDispatch::ParamsParser#parse_formatted_parameters` to rewind body input stream on
- parsing json params.
-
- Fixes #11345.
-
- *Yuri Bol*, *Paul Nikitochkin*
-
-* Ignore spaces around delimiter in Set-Cookie header.
-
- *Yamagishi Kazutoshi*
-
-* Remove deprecated Rails application fallback for integration testing, set
- `ActionDispatch.test_app` instead.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated `page_cache_extension` config.
-
- *Francesco Rodriguez*
-
-* Remove deprecated constants from Action Controller:
-
- 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
-
- *Carlos Antonio da Silva*
-
-* 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.
-
- Fixes #10965.
-
- *Becker*
-
-* 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).
-
- Fixes #9856.
+* Make logging of CSRF failures optional (but on by default) with the
+ `log_warning_on_csrf_failure` configuration setting in
+ `ActionController::RequestForgeryProtection`.
- *Yves Senn*, *Andrew White*
+ *John Barton*
-* Action View extracted from Action Pack.
+* Fix URL generation in controller tests with request-dependent
+ `default_url_options` methods.
- *Piotr Sarnacki*, *Łukasz Strzałkowski*
+ *Tony Wooster*
-Please check [4-0-stable](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/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index 2f6575c3b5..02a24a7412 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -48,6 +48,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc
index ad1448f61b..f96a9d9da5 100644
--- a/actionpack/RUNNING_UNIT_TESTS.rdoc
+++ b/actionpack/RUNNING_UNIT_TESTS.rdoc
@@ -4,14 +4,14 @@ 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, 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://docs.seattlerb.org/rake/.
== 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"
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index 1d6009bab8..d509891fe3 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
- s.add_dependency 'rack', '~> 1.5.2'
+ s.add_dependency 'rack', '~> 1.6.0.alpha'
s.add_dependency 'rack-test', '~> 0.6.2'
s.add_dependency 'actionview', version
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index af5de815bb..4026dab2ce 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -8,7 +8,8 @@ module AbstractController
class Error < StandardError #:nodoc:
end
- class ActionNotFound < StandardError #:nodoc:
+ # Raised when a non-existing controller action is triggered.
+ class ActionNotFound < StandardError
end
# <tt>AbstractController::Base</tt> is a low-level API. Nobody should be
@@ -120,14 +121,14 @@ module AbstractController
#
# The actual method that is called is determined by calling
# #method_for_action. If no method can handle the action, then an
- # ActionNotFound error is raised.
+ # AbstractController::ActionNotFound error is raised.
#
# ==== Returns
# * <tt>self</tt>
def process(action, *args)
- @_action_name = action_name = action.to_s
+ @_action_name = action.to_s
- unless action_name = method_for_action(action_name)
+ unless action_name = _find_action_name(@_action_name)
raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}"
end
@@ -160,7 +161,15 @@ module AbstractController
# ==== Returns
# * <tt>TrueClass</tt>, <tt>FalseClass</tt>
def available_action?(action_name)
- method_for_action(action_name).present?
+ _find_action_name(action_name).present?
+ end
+
+ # Returns true if the given controller is capable of rendering
+ # a path. A subclass of +AbstractController::Base+
+ # may return false. An Email controller for example does not
+ # support paths, only full URLs.
+ def self.supports_path?
+ true
end
private
@@ -204,6 +213,24 @@ module AbstractController
end
# Takes an action name and returns the name of the method that will
+ # handle the action.
+ #
+ # It checks if the action name is valid and returns false otherwise.
+ #
+ # See method_for_action for more information.
+ #
+ # ==== Parameters
+ # * <tt>action_name</tt> - An action name to find a method name for
+ #
+ # ==== Returns
+ # * <tt>string</tt> - The name of the method that handles the action
+ # * false - No valid method name could be found.
+ # Raise AbstractController::ActionNotFound.
+ def _find_action_name(action_name)
+ _valid_action_name?(action_name) && method_for_action(action_name)
+ end
+
+ # Takes an action name and returns the name of the method that will
# handle the action. In normal cases, this method returns the same
# name as it receives. By default, if #method_for_action receives
# a name that is not an action, it will look for an #action_missing
@@ -218,14 +245,14 @@ module AbstractController
# the case.
#
# If none of these conditions are true, and method_for_action
- # returns nil, an ActionNotFound exception will be raised.
+ # returns nil, an AbstractController::ActionNotFound exception will be raised.
#
# ==== Parameters
# * <tt>action_name</tt> - An action name to find a method name for
#
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
- # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound.
+ # * <tt>nil</tt> - No method name could be found.
def method_for_action(action_name)
if action_method?(action_name)
action_name
@@ -233,5 +260,10 @@ module AbstractController
"_handle_action_missing"
end
end
+
+ # Checks if the action name is valid and returns false otherwise.
+ def _valid_action_name?(action_name)
+ !action_name.to_s.include? File::SEPARATOR
+ end
end
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index d6c941832f..ca5c80cd71 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -42,20 +42,18 @@ module AbstractController
end
end
- # Skip before, after, and around action callbacks matching any of the names
- # Aliased as skip_filter.
+ # Skip before, after, and around action callbacks matching any of the names.
#
# ==== Parameters
# * <tt>names</tt> - A list of valid names that could be used for
# callbacks. Note that skipping uses Ruby equality, so it's
# impossible to skip a callback defined using an anonymous proc
- # using #skip_filter
+ # using #skip_action_callback
def skip_action_callback(*names)
skip_before_action(*names)
skip_after_action(*names)
skip_around_action(*names)
end
-
alias_method :skip_filter, :skip_action_callback
# Take callback names and an optional callback proc, normalize them,
@@ -85,7 +83,6 @@ module AbstractController
# :call-seq: before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as before_filter.
##
# :method: prepend_before_action
@@ -93,7 +90,6 @@ module AbstractController
# :call-seq: prepend_before_action(names, block)
#
# Prepend a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as prepend_before_filter.
##
# :method: skip_before_action
@@ -101,7 +97,6 @@ module AbstractController
# :call-seq: skip_before_action(names)
#
# Skip a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as skip_before_filter.
##
# :method: append_before_action
@@ -109,7 +104,6 @@ module AbstractController
# :call-seq: append_before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as append_before_filter.
##
# :method: after_action
@@ -117,7 +111,6 @@ module AbstractController
# :call-seq: after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as after_filter.
##
# :method: prepend_after_action
@@ -125,7 +118,6 @@ module AbstractController
# :call-seq: prepend_after_action(names, block)
#
# Prepend a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as prepend_after_filter.
##
# :method: skip_after_action
@@ -133,7 +125,6 @@ module AbstractController
# :call-seq: skip_after_action(names)
#
# Skip a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as skip_after_filter.
##
# :method: append_after_action
@@ -141,7 +132,6 @@ module AbstractController
# :call-seq: append_after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as append_after_filter.
##
# :method: around_action
@@ -149,7 +139,6 @@ module AbstractController
# :call-seq: around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as around_filter.
##
# :method: prepend_around_action
@@ -157,7 +146,6 @@ module AbstractController
# :call-seq: prepend_around_action(names, block)
#
# Prepend a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as prepend_around_filter.
##
# :method: skip_around_action
@@ -165,7 +153,6 @@ module AbstractController
# :call-seq: skip_around_action(names)
#
# Skip a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as skip_around_filter.
##
# :method: append_around_action
@@ -173,46 +160,36 @@ module AbstractController
# :call-seq: append_around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as append_around_filter.
# 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_#{callback}_filter", :"#{callback}_action"
end
end
end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index e77e4e01e9..95c67d482b 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -27,9 +27,6 @@ module AbstractController
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.
diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
index 6684f46f64..568c47e43a 100644
--- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -1,14 +1,14 @@
module AbstractController
module Railties
module RoutesHelpers
- def self.with(routes)
+ def self.with(routes, include_path_helpers = true)
Module.new do
define_method(:inherited) do |klass|
super(klass)
if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
- klass.send(:include, namespace.railtie_routes_url_helpers)
+ klass.send(:include, namespace.railtie_routes_url_helpers(include_path_helpers))
else
- klass.send(:include, routes.url_helpers)
+ klass.send(:include, routes.url_helpers(include_path_helpers))
end
end
end
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index f24b03ad16..9d10140ed2 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -23,7 +23,7 @@ module AbstractController
def render(*args, &block)
options = _normalize_render(*args, &block)
self.response_body = render_to_body(options)
- _process_format(rendered_format) if rendered_format
+ _process_format(rendered_format, options) if rendered_format
self.response_body
end
@@ -98,7 +98,7 @@ module AbstractController
# Process the rendered format.
# :api: private
- def _process_format(format)
+ def _process_format(format, options = {})
end
# Normalize args and options.
@@ -106,7 +106,9 @@ module AbstractController
def _normalize_render(*args, &block)
options = _normalize_args(*args, &block)
#TODO: remove defined? when we restore AP <=> AV dependency
- options[:variant] = request.variant if defined?(request) && request.variant.present?
+ if defined?(request) && request && request.variant.present?
+ options[:variant] = request.variant
+ end
_normalize_options(options)
options
end
diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb
index 4a95e1f276..72d07b0927 100644
--- a/actionpack/lib/abstract_controller/url_for.rb
+++ b/actionpack/lib/abstract_controller/url_for.rb
@@ -11,7 +11,7 @@ module AbstractController
def _routes
raise "In order to use #url_for, you must include routing helpers explicitly. " \
- "For instance, `include Rails.application.routes.url_helpers"
+ "For instance, `include Rails.application.routes.url_helpers`."
end
module ClassMethods
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index c0f10da23a..e6fe6b0b00 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -45,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
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 12d798d0c1..de85e0c1a7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -16,7 +16,7 @@ module ActionController
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching.
#
- # Configuration examples (MemoryStore is the default):
+ # Configuration examples (FileStore is the default):
#
# config.action_controller.cache_store = :memory_store
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb
index 879d5fdd94..2694d4c12f 100644
--- a/actionpack/lib/action_controller/caching/fragments.rb
+++ b/actionpack/lib/action_controller/caching/fragments.rb
@@ -90,7 +90,13 @@ module ActionController
end
def instrument_fragment_cache(name, key) # :nodoc:
- ActiveSupport::Notifications.instrument("#{name}.action_controller", :key => key){ yield }
+ payload = {
+ controller: controller_name,
+ action: action_name,
+ key: key
+ }
+
+ ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield }
end
end
end
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index 823a1050b5..89fa75f025 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -16,50 +16,51 @@ module ActionController
end
def process_action(event)
- return unless logger.info?
-
- payload = event.payload
- additions = ActionController::Base.log_process_action(payload)
-
- status = payload[:status]
- if status.nil? && payload[:exception].present?
- exception_class_name = payload[:exception].first
- status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
+ info do
+ payload = event.payload
+ additions = ActionController::Base.log_process_action(payload)
+
+ status = payload[:status]
+ if status.nil? && payload[:exception].present?
+ exception_class_name = payload[:exception].first
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
+ end
+ message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
+ message << " (#{additions.join(" | ")})" unless additions.blank?
+ message
end
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
- message << " (#{additions.join(" | ")})" unless additions.blank?
-
- info(message)
end
def halted_callback(event)
- info("Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected")
+ info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
end
def send_file(event)
- info("Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)")
+ info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
end
def redirect_to(event)
- info("Redirected to #{event.payload[:location]}")
+ info { "Redirected to #{event.payload[:location]}" }
end
def send_data(event)
- info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)")
+ info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
end
def unpermitted_parameters(event)
- unpermitted_keys = event.payload[:keys]
- debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}")
+ debug do
+ unpermitted_keys = event.payload[:keys]
+ "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
+ end
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)
+ debug do
+ "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."\
+ end
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..bfbc15a901 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -30,10 +30,8 @@ module ActionController
end
end
- def build(action, app=nil, &block)
- app ||= block
+ def build(action, app = Proc.new)
action = action.to_s
- raise "MiddlewareStack#build requires an app" unless app
middlewares.reverse.inject(app) do |a, middleware|
middleware.valid?(action) ? middleware.build(a) : a
@@ -70,7 +68,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
@@ -183,7 +182,8 @@ module ActionController
body = [body] unless body.nil? || body.respond_to?(:each)
super
end
-
+
+ # Tests if render or redirect has already happened.
def performed?
response_body || (response && response.committed?)
end
@@ -222,14 +222,23 @@ module ActionController
# Makes the controller a Rack endpoint that runs the action in the given
# +env+'s +action_dispatch.request.path_parameters+ key.
def self.call(env)
- action(env['action_dispatch.request.path_parameters'][:action]).call(env)
+ req = ActionDispatch::Request.new env
+ action(req.path_parameters[:action]).call(env)
end
# Returns a Rack endpoint for the given action name.
def self.action(name, klass = ActionDispatch::Request)
- middleware_stack.build(name.to_s) do |env|
- new.dispatch(name, klass.new(env))
+ if middleware_stack.any?
+ middleware_stack.build(name) do |env|
+ new.dispatch(name, klass.new(env))
+ end
+ else
+ lambda { |env| new.dispatch(name, klass.new(env)) }
end
end
+
+ def _status_code
+ @_status
+ end
end
end
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/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index a2cb6d1e66..d920668184 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -85,7 +85,7 @@ module ActionController
if host_or_options.is_a?(Hash)
options.merge!(host_or_options)
elsif host_or_options
- options.merge!(:host => host_or_options)
+ options[:host] = host_or_options
end
secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 43407f5b78..3d2badf9c2 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -14,6 +14,8 @@ module ActionController
# return head(:method_not_allowed) unless request.post?
# return head(:bad_request) unless valid_request?
# render
+ #
+ # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
def head(status, options = {})
options, status = status, nil if status.is_a?(Hash)
status ||= options.delete(:status) || :ok
@@ -27,7 +29,7 @@ module ActionController
self.status = status
self.location = url_for(location) if location
- if include_content?(self.status)
+ if include_content?(self._status_code)
self.content_type = content_type || (Mime[formats.first] if formats)
self.response.charset = false if self.response
self.response_body = " "
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 158d552ec7..25c123edf7 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
#
@@ -90,17 +90,29 @@ module ActionController
end
def authenticate(request, &login_procedure)
- unless request.authorization.blank?
+ if has_basic_credentials?(request)
login_procedure.call(*user_name_and_password(request))
end
end
+ def has_basic_credentials?(request)
+ request.authorization.present? && (auth_scheme(request) == 'Basic')
+ end
+
def user_name_and_password(request)
- decode_credentials(request).split(/:/, 2)
+ decode_credentials(request).split(':', 2)
end
def decode_credentials(request)
- ::Base64.decode64(request.authorization.split(' ', 2).last || '')
+ ::Base64.decode64(auth_param(request) || '')
+ end
+
+ def auth_scheme(request)
+ request.authorization.split(' ', 2).first
+ end
+
+ def auth_param(request)
+ request.authorization.split(' ', 2).second
end
def encode_credentials(user_name, password)
@@ -109,8 +121,8 @@ module ActionController
def authentication_request(controller, realm)
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
- controller.response_body = "HTTP Basic: Access denied.\n"
controller.status = 401
+ controller.response_body = "HTTP Basic: Access denied.\n"
end
end
@@ -127,11 +139,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
@@ -244,8 +256,8 @@ module ActionController
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Digest: Access denied.\n"
authentication_header(controller, realm)
- controller.response_body = message
controller.status = 401
+ controller.response_body = message
end
def secret_token(request)
@@ -321,11 +333,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
@@ -437,7 +449,7 @@ module ActionController
authorization_request = request.authorization.to_s
if authorization_request[TOKEN_REGEX]
params = token_params_from authorization_request
- [params.shift.last, Hash[params].with_indifferent_access]
+ [params.shift[1], Hash[params].with_indifferent_access]
end
end
@@ -452,14 +464,14 @@ module ActionController
# This removes the `"` characters wrapping the value.
def rewrite_param_values(array_params)
- array_params.each { |param| param.last.gsub! %r/^"|"$/, '' }
+ array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' }
end
# This method takes an authorization body and splits up the key-value
# pairs by the standardized `:`, `;`, or `\t` delimiters defined in
# `AUTHN_PAIR_DELIMITERS`.
def raw_params(auth)
- auth.sub(TOKEN_REGEX, '').split(/"\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
+ auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
end
# Encodes the given token and options into an Authorization header value.
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 33014b97ca..c9ef3a3dad 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -102,13 +102,30 @@ module ActionController
end
end
- @stream.write "data: #{json}\n\n"
+ message = json.gsub("\n", "\ndata: ")
+ @stream.write "data: #{message}\n\n"
end
end
+ class ClientDisconnected < RuntimeError
+ end
+
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
+ include MonitorMixin
+
+ # Ignore that the client has disconnected.
+ #
+ # If this value is `true`, calling `write` after the client
+ # disconnects will result in the written content being silently
+ # discarded. If this value is `false` (the default), a
+ # ClientDisconnected exception will be raised.
+ attr_accessor :ignore_disconnect
+
def initialize(response)
- @error_callback = nil
+ @error_callback = lambda { true }
+ @cv = new_cond
+ @aborted = false
+ @ignore_disconnect = false
super(response, SizedQueue.new(10))
end
@@ -119,17 +136,63 @@ module ActionController
end
super
+
+ unless connected?
+ @buf.clear
+
+ unless @ignore_disconnect
+ # Raise ClientDisconnected, which is a RuntimeError (not an
+ # IOError), because that's more appropriate for something beyond
+ # the developer's control.
+ raise ClientDisconnected, "client disconnected"
+ end
+ end
end
def each
+ @response.sending!
while str = @buf.pop
yield str
end
+ @response.sent!
end
+ # Write a 'close' event to the buffer; the producer/writing thread
+ # uses this to notify us that it's finished supplying content.
+ #
+ # See also #abort.
def close
- super
- @buf.push nil
+ synchronize do
+ super
+ @buf.push nil
+ @cv.broadcast
+ end
+ end
+
+ # Inform the producer/writing thread that the client has
+ # disconnected; the reading thread is no longer interested in
+ # anything that's being written.
+ #
+ # See also #close.
+ def abort
+ synchronize do
+ @aborted = true
+ @buf.clear
+ end
+ end
+
+ # Is the client still connected and waiting for content?
+ #
+ # The result of calling `write` when this is `false` is determined
+ # by `ignore_disconnect`.
+ def connected?
+ !@aborted
+ end
+
+ def await_close
+ synchronize do
+ @cv.wait_until { @closed }
+ end
end
def on_error(&block)
@@ -142,7 +205,7 @@ module ActionController
end
class Response < ActionDispatch::Response #:nodoc: all
- class Header < DelegateClass(Hash)
+ class Header < DelegateClass(Hash) # :nodoc:
def initialize(response, header)
@response = response
super(header)
@@ -165,12 +228,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
@@ -191,6 +262,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
@@ -205,14 +277,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!
@@ -220,16 +296,19 @@ module ActionController
}
@_response.await_commit
+ raise error if error
end
def log_error(exception)
logger = ActionController::Base.logger
return unless logger
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << exception.backtrace.join("\n ")
- logger.fatal("#{message}\n\n")
+ logger.fatal do
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ "#{message}\n\n"
+ end
end
def response_body=(body)
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 1974bbf529..00e7e980f8 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -322,7 +322,7 @@ module ActionController #:nodoc:
# end
# end
#
- # * for a javascript request - if the template isn't found, an exception is
+ # * for a JavaScript request - if the template isn't found, an exception is
# raised.
# * 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>,
@@ -335,7 +335,7 @@ module ActionController #:nodoc:
# As outlined above, the +resources+ argument passed to +respond_with+
# can play two roles. It can be used to generate the redirect url
# for successful html requests (e.g. for +create+ actions when
- # no template exists), while for formats other than html and javascript
+ # no template exists), while for formats other than html and JavaScript
# it is the object that gets rendered, by being converted directly to the
# required format (again assuming no template exists).
#
@@ -352,7 +352,7 @@ module ActionController #:nodoc:
#
# This would cause +respond_with+ to redirect to <code>project_task_url</code>
# instead of <code>task_url</code>. For request formats other than html or
- # javascript, if multiple resources are passed in this way, it is the last
+ # JavaScript, if multiple resources are passed in this way, it is the last
# one specified that is rendered.
#
# === Customizing response behavior
diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb
index bdf6e88699..6921834044 100644
--- a/actionpack/lib/action_controller/metal/rack_delegation.rb
+++ b/actionpack/lib/action_controller/metal/rack_delegation.rb
@@ -6,7 +6,7 @@ module ActionController
extend ActiveSupport::Concern
delegate :headers, :status=, :location=, :content_type=,
- :status, :location, :content_type, :to => "@_response"
+ :status, :location, :content_type, :_status_code, :to => "@_response"
def dispatch(action, request)
set_response!(request)
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index 2812038938..acaa8227c9 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -14,7 +14,7 @@ module ActionController
include ActionController::RackDelegation
include ActionController::UrlFor
- # Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
+ # Redirects the browser to the target specified in +options+. This parameter can be any one of:
#
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
@@ -24,6 +24,8 @@ module ActionController
# * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
# Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
#
+ # === Examples:
+ #
# redirect_to action: "show", id: 5
# redirect_to post
# redirect_to "http://www.rubyonrails.org"
@@ -32,7 +34,7 @@ module ActionController
# redirect_to :back
# redirect_to proc { edit_post_url(@post) }
#
- # The redirection happens as a "302 Found" header unless otherwise specified.
+ # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option:
#
# redirect_to post_url(@post), status: :found
# redirect_to action: 'atom', status: :moved_permanently
@@ -60,18 +62,21 @@ module ActionController
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# 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.
+ # When using <tt>redirect_to :back</tt>, if there is no referrer,
+ # <tt>ActionController::RedirectBackError</tt> will be raised. You
+ # may specify some fallback behavior for this case by rescuing
+ # <tt>ActionController::RedirectBackError</tt>.
def redirect_to(options = {}, response_status = {}) #:doc:
raise ActionControllerError.new("Cannot redirect to nil!") unless options
+ raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters)
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_status)
- self.location = _compute_redirect_to_location(options)
- self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
+ self.location = _compute_redirect_to_location(request, options)
+ self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
end
- def _compute_redirect_to_location(options) #:nodoc:
+ def _compute_redirect_to_location(request, options) #:nodoc:
case options
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
@@ -85,11 +90,13 @@ module ActionController
when :back
request.headers["Referer"] or raise RedirectBackError
when Proc
- _compute_redirect_to_location options.call
+ _compute_redirect_to_location request, options.call
else
url_for(options)
end.delete("\0\r\n")
end
+ module_function :_compute_redirect_to_location
+ public :_compute_redirect_to_location
private
def _extract_redirect_to_status(options, response_status)
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 6c7b4652d4..02c4e563f5 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -6,6 +6,11 @@ module ActionController
Renderers.add(key, &block)
end
+ # See <tt>Renderers.remove</tt>
+ def self.remove_renderer(key)
+ Renderers.remove(key)
+ end
+
class MissingRenderer < LoadError
def initialize(format)
super "No renderer defined for format: #{format}"
@@ -42,8 +47,8 @@ module ActionController
nil
end
- # Hash of available renderers, mapping a renderer name to its proc.
- # Default keys are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
+ # 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.
@@ -73,7 +78,7 @@ module ActionController
# respond_to do |format|
# format.html
# format.csv { render csv: @csvable, filename: @csvable.name }
- # }
+ # end
# end
# To use renderers and their mime types in more concise ways, see
# <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and
@@ -83,6 +88,17 @@ module ActionController
RENDERERS << key.to_sym
end
+ # This method is the opposite of add method.
+ #
+ # Usage:
+ #
+ # ActionController::Renderers.remove(:csv)
+ def self.remove(key)
+ RENDERERS.delete(key.to_sym)
+ method = "_render_option_#{key}"
+ remove_method(method) if method_defined?(method)
+ end
+
module All
extend ActiveSupport::Concern
include Renderers
@@ -96,8 +112,11 @@ module ActionController
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
- self.content_type ||= Mime::JS
- "#{options[:callback]}(#{json})"
+ if self.content_type.nil? || self.content_type == Mime::JSON
+ self.content_type = Mime::JS
+ end
+
+ "/**/#{options[:callback]}(#{json})"
else
self.content_type ||= Mime::JSON
json
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index 5c48b4ab98..7bbff0450a 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -2,6 +2,8 @@ module ActionController
module Rendering
extend ActiveSupport::Concern
+ RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
+
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
self.formats = request.formats.map(&:ref).compact
@@ -27,14 +29,27 @@ module ActionController
end
def render_to_body(options = {})
- super || options[:text].presence || ' '
+ super || _render_in_priorities(options) || ' '
end
private
- def _process_format(format)
+ 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
- self.content_type ||= format.to_s
+
+ 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.
@@ -46,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)
+ options[:body] = nil
end
if options[:status]
@@ -61,6 +78,14 @@ 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
+
# 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 c88074d4c6..0efa0fb259 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -68,12 +68,16 @@ 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
module ClassMethods
- # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked.
+ # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
#
# class ApplicationController < ActionController::Base
# protect_from_forgery
@@ -193,7 +197,9 @@ module ActionController #:nodoc:
mark_for_same_origin_verification!
if !verified_request?
- logger.warn "Can't verify CSRF token authenticity" if logger
+ if logger && log_warning_on_csrf_failure
+ logger.warn "Can't verify CSRF token authenticity"
+ end
handle_unverified_request
end
end
@@ -241,7 +247,7 @@ module ActionController #:nodoc:
# * Does the X-CSRF-Token header match the form_authenticity_token
def verified_request?
!protect_against_forgery? || request.get? || request.head? ||
- form_authenticity_token == params[request_forgery_protection_token] ||
+ form_authenticity_token == form_authenticity_param ||
form_authenticity_token == request.headers['X-CSRF-Token']
end
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
index e24b56fa91..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.
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 62d5931b45..04401cad7b 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -183,7 +183,7 @@ module ActionController #:nodoc:
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
# Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
#
- # If you are using Unicorn with Nginx, you may need to tweak Nginx.
+ # If you are using Unicorn with NGINX, you may need to tweak NGINX.
# Streaming should work out of the box on Rainbows.
#
# ==== Passenger
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 48a916f2b1..bc27ecaa20 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/array/wrap'
+require 'active_support/deprecation'
require 'active_support/rescuable'
require 'action_dispatch/http/upload'
require 'stringio'
@@ -32,14 +33,14 @@ 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
# == Action Controller \Parameters
#
# Allows to choose which attributes should be whitelisted for mass updating
- # and thus prevent accidentally exposing that which shouldn’t be exposed.
+ # and thus prevent accidentally exposing that which shouldn't be exposed.
# Provides two methods for this purpose: #require and #permit. The former is
# used to mark parameters as required. The latter is used to set the parameter
# as permitted and limit which attributes should be allowed for mass updating.
@@ -101,9 +102,23 @@ module ActionController
cattr_accessor :permit_all_parameters, instance_accessor: false
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
- # Never raise an UnpermittedParameters exception because of these params
- # are present. They are added by Rails and it's of no concern.
- NEVER_UNPERMITTED_PARAMS = %w( controller action )
+ # By default, never raise an UnpermittedParameters exception if these
+ # params are present. The default includes both 'controller' and 'action'
+ # because they are added by Rails and should be of no concern. One way
+ # to change these is to specify `always_permitted_parameters` in your
+ # config. For instance:
+ #
+ # config.always_permitted_parameters = %w( controller action format )
+ cattr_accessor :always_permitted_parameters
+ self.always_permitted_parameters = %w( controller action )
+
+ def self.const_missing(const_name)
+ super unless const_name == :NEVER_UNPERMITTED_PARAMS
+ ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\
+ " has been deprecated. Use "\
+ "`ActionController::Parameters.always_permitted_parameters` instead."
+ self.always_permitted_parameters
+ end
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
@@ -129,6 +144,10 @@ module ActionController
# 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.
+ #
+ # Testing membership still loops, but it's going to be faster than our own
+ # loop that converts values. Also, we are not going to build a new array
+ # object per fetch.
def converted_arrays
@converted_arrays ||= Set.new
end
@@ -158,8 +177,8 @@ module ActionController
def permit!
each_pair do |key, value|
value = convert_hashes_to_parameters(key, value)
- Array.wrap(value).each do |_|
- _.permit! if _.respond_to? :permit!
+ Array.wrap(value).each do |v|
+ v.permit! if v.respond_to? :permit!
end
end
@@ -180,7 +199,12 @@ module ActionController
# ActionController::Parameters.new(person: {}).require(:person)
# # => ActionController::ParameterMissing: param not found: person
def require(key)
- self[key].presence || raise(ParameterMissing.new(key))
+ value = self[key]
+ if value.present? || value == false
+ value
+ else
+ raise ParameterMissing.new(key)
+ end
end
# Alias of #require.
@@ -380,7 +404,7 @@ module ActionController
end
def unpermitted_keys(params)
- self.keys - params.keys - NEVER_UNPERMITTED_PARAMS
+ self.keys - params.keys - self.always_permitted_parameters
end
#
@@ -502,7 +526,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..0f2fa5fb08 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -23,25 +23,24 @@ module ActionController
include AbstractController::UrlFor
def url_options
- @_url_options ||= super.reverse_merge(
+ @_url_options ||= {
:host => request.host,
:port => request.optional_port,
:protocol => request.protocol,
- :_recall => request.symbolized_path_parameters
- ).freeze
+ :_recall => request.path_parameters
+ }.merge!(super).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
- options[:original_script_name] = original_script_name
- else
- options[:script_name] = same_origin ? request.script_name.dup : script_name
- end
- options.freeze
+ options = @_url_options.dup
+ if original_script_name
+ options[:original_script_name] = original_script_name
+ else
+ options[:script_name] = same_origin ? request.script_name.dup : script_name
end
+ options.freeze
else
@_url_options
end
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index a2fc814221..28b20052b5 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -23,6 +23,10 @@ module ActionController
options = app.config.action_controller
ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
+ if app.config.action_controller[:always_permitted_parameters]
+ ActionController::Parameters.always_permitted_parameters =
+ app.config.action_controller.delete(:always_permitted_parameters)
+ end
ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
(Rails.env.test? || Rails.env.development?) ? :log : false
end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index cf11ce1a9b..152420a54c 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -12,13 +12,16 @@ module ActionController
teardown :teardown_subscriptions
end
+ RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze
+
def setup_subscriptions
- @_partials = Hash.new(0)
- @_templates = Hash.new(0)
- @_layouts = Hash.new(0)
- @_files = Hash.new(0)
+ RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
+ instance_variable_set("@_#{instance_variable}", Hash.new(0))
+ end
+
+ @_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,42 +31,43 @@ module ActionController
end
end
- ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
- path = payload[:virtual_path]
- next unless path
- partial = path =~ /^.*\/_[^\/]*$/
-
- if partial
- @_partials[path] += 1
- @_partials[path.split("/").last] += 1
- end
+ @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
+ if virtual_path = payload[:virtual_path]
+ partial = virtual_path =~ /^.*\/_[^\/]*$/
- @_templates[path] += 1
- end
-
- ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
- next if payload[:virtual_path] # files don't have virtual path
+ if partial
+ @_partials[virtual_path] += 1
+ @_partials[virtual_path.split("/").last] += 1
+ end
- path = payload[:identifier]
- if path
- @_files[path] += 1
- @_files[path.split("/").last] += 1
+ @_templates[virtual_path] += 1
+ else
+ path = payload[:identifier]
+ if path
+ @_files[path] += 1
+ @_files[path.split("/").last] += 1
+ end
end
end
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)
- @_partials = Hash.new(0)
- @_templates = Hash.new(0)
- @_layouts = Hash.new(0)
+ reset_template_assertion
super
end
+ def reset_template_assertion
+ RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
+ instance_variable_get("@_#{instance_variable}").clear
+ end
+ end
+
# Asserts that the request was rendered with the appropriate template file or partials.
#
# # assert that the "new" view template was rendered
@@ -87,6 +91,13 @@ module ActionController
# # assert that no partials were rendered
# assert_template partial: false
#
+ # # assert that a file was rendered
+ # assert_template file: "README.rdoc"
+ #
+ # # assert that no file was rendered
+ # assert_template file: nil
+ # assert_template file: false
+ #
# In a view test case, you can also assert that specific locals are passed
# to partials:
#
@@ -136,6 +147,8 @@ module ActionController
if options[:file]
assert_includes @_files.keys, options[:file]
+ elsif options.key?(:file)
+ assert @_files.blank?, "expected no files but #{@_files.keys} was rendered"
end
if expected_partial = options[:partial]
@@ -197,7 +210,7 @@ module ActionController
value = value.dup
end
- if extra_keys.include?(key.to_sym)
+ if extra_keys.include?(key)
non_path_parameters[key] = value
else
if value.is_a?(Array)
@@ -206,7 +219,7 @@ module ActionController
value = value.to_param
end
- path_parameters[key.to_s] = value
+ path_parameters[key] = value
end
end
@@ -231,7 +244,6 @@ module ActionController
@formats = nil
@env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
@env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
- @symbolized_path_params = nil
@method = @request_method = nil
@fullpath = @ip = @remote_ip = @protocol = nil
@env['action_dispatch.request.query_parameters'] = {}
@@ -258,6 +270,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:
@@ -430,7 +465,6 @@ module ActionController
end
def controller_class=(new_class)
- prepare_controller_class(new_class) if new_class
self._controller_class = new_class
end
@@ -447,11 +481,6 @@ module ActionController
Class === constant && constant < ActionController::Metal
end
end
-
- def prepare_controller_class(new_class)
- new_class.send :include, ActionController::TestCase::RaiseActionExceptions
- end
-
end
# Simulate a GET request with the given parameters.
@@ -463,8 +492,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.
@@ -525,6 +554,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
@@ -533,6 +587,7 @@ module ActionController
end
parameters, session, flash = args
+ parameters ||= {}
# Ensure that numbers and symbols passed as params are converted to
# proper params, as is the case when engaging rack.
@@ -542,7 +597,6 @@ module ActionController
unless @controller.respond_to?(:recycle!)
@controller.extend(Testing::Functional)
- @controller.class.class_eval { include Testing }
end
@request.recycle!
@@ -551,7 +605,6 @@ module ActionController
@request.env['REQUEST_METHOD'] = http_method
- parameters ||= {}
controller_class_name = @controller.class.anonymous? ?
"anonymous" :
@controller.class.controller_path
@@ -568,27 +621,34 @@ 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!
@assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {}
- @request.session['flash'] = @request.flash.to_session_value
- @request.session.delete('flash') if @request.session['flash'].blank?
+
+ if flash_value = @request.flash.to_session_value
+ @request.session['flash'] = flash_value
+ end
+
@response
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
@@ -598,6 +658,10 @@ module ActionController
end
end
+ @request = build_request
+ @response = build_response response_klass
+ @response.request = @request
+
if @controller
@controller.request = @request
@controller.params = {}
@@ -608,8 +672,8 @@ module ActionController
TestRequest.new
end
- def build_response
- TestResponse.new
+ def build_response(klass)
+ klass.new
end
included do
@@ -634,12 +698,11 @@ module ActionController
unless @request.env["PATH_INFO"]
options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
options.update(
- :only_path => true,
:action => action,
:relative_url_root => nil,
- :_recall => @request.symbolized_path_parameters)
+ :_recall => @request.path_parameters)
- url, query_string = @routes.url_for(options).split("?", 2)
+ url, query_string = @routes.path_for(options).split("?", 2)
@request.env["SCRIPT_NAME"] = @controller.config.relative_url_root
@request.env["PATH_INFO"] = url
@@ -648,39 +711,11 @@ module ActionController
end
def html_format?(parameters)
- return true unless parameters.is_a?(Hash)
+ return true unless parameters.key?(:format)
Mime.fetch(parameters[:format]) { Mime['html'] }.html?
end
end
- # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
- # (skipping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
- # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
- # than 0.0.0.0.
- #
- # The exception is stored in the exception accessor for further inspection.
- module RaiseActionExceptions
- def self.included(base) #:nodoc:
- unless base.method_defined?(:exception) && base.method_defined?(:exception=)
- base.class_eval do
- attr_accessor :exception
- protected :exception, :exception=
- end
- end
- end
-
- protected
- def rescue_action_without_handler(e)
- self.exception = e
-
- if request.remote_addr == "0.0.0.0"
- raise(e)
- else
- super(e)
- end
- end
- end
-
include Behavior
end
end
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 3dd2e2a45c..11b5e6be33 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -52,7 +52,6 @@ module ActionDispatch
autoload :DebugExceptions
autoload :ExceptionWrapper
autoload :Flash
- autoload :Head
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index f9b278349e..63a3cbc90b 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -92,7 +92,7 @@ module ActionDispatch
LAST_MODIFIED = "Last-Modified".freeze
ETAG = "ETag".freeze
CACHE_CONTROL = "Cache-Control".freeze
- SPECIAL_KEYS = %w[extras no-cache max-age public must-revalidate]
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
def cache_control_segments
if cache_control = self[CACHE_CONTROL]
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/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 2666cd4b0a..bc5410dc38 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -1,34 +1,63 @@
module ActionDispatch
module Http
+ # Provides access to the request's HTTP headers from the environment.
+ #
+ # env = { "CONTENT_TYPE" => "text/plain" }
+ # headers = ActionDispatch::Http::Headers.new(env)
+ # headers["Content-Type"] # => "text/plain"
class Headers
- CGI_VARIABLES = %w(
- CONTENT_TYPE CONTENT_LENGTH
- HTTPS AUTH_TYPE GATEWAY_INTERFACE
- PATH_INFO PATH_TRANSLATED QUERY_STRING
- REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER
- REQUEST_METHOD SCRIPT_NAME
- SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE
- )
+ CGI_VARIABLES = Set.new(%W[
+ AUTH_TYPE
+ CONTENT_LENGTH
+ CONTENT_TYPE
+ GATEWAY_INTERFACE
+ HTTPS
+ PATH_INFO
+ PATH_TRANSLATED
+ QUERY_STRING
+ REMOTE_ADDR
+ REMOTE_HOST
+ REMOTE_IDENT
+ REMOTE_USER
+ REQUEST_METHOD
+ SCRIPT_NAME
+ SERVER_NAME
+ SERVER_PORT
+ SERVER_PROTOCOL
+ SERVER_SOFTWARE
+ ]).freeze
+
HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
include Enumerable
attr_reader :env
- def initialize(env = {})
+ def initialize(env = {}) # :nodoc:
@env = env
end
+ # Returns the value for the given key mapped to @env.
def [](key)
@env[env_name(key)]
end
+ # Sets the given value for the key mapped to @env.
def []=(key, value)
@env[env_name(key)] = value
end
- def key?(key); @env.key? key; end
+ def key?(key)
+ @env.key? env_name(key)
+ end
alias :include? :key?
+ # Returns the value for the given key mapped to @env.
+ #
+ # If the key is not found and an optional code block is not provided,
+ # raises a <tt>KeyError</tt> exception.
+ #
+ # If the code block is provided, then it will be run and
+ # its result returned.
def fetch(key, *args, &block)
@env.fetch env_name(key), *args, &block
end
@@ -37,12 +66,17 @@ module ActionDispatch
@env.each(&block)
end
+ # Returns a new Http::Headers instance containing the contents of
+ # <tt>headers_or_env</tt> and the original instance.
def merge(headers_or_env)
headers = Http::Headers.new(env.dup)
headers.merge!(headers_or_env)
headers
end
+ # Adds the contents of <tt>headers_or_env</tt> to original instance
+ # entries; duplicate keys are overwritten with the values from
+ # <tt>headers_or_env</tt>.
def merge!(headers_or_env)
headers_or_env.each do |key, value|
self[env_name(key)] = value
@@ -50,6 +84,8 @@ module ActionDispatch
end
private
+ # Converts a HTTP header name to an environment variable name if it is
+ # not contained within the headers hash.
def env_name(key)
key = key.to_s
if key =~ HTTP_HEADER
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index b803ce8b6f..9c8f65deac 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -54,8 +54,14 @@ module ActionDispatch
end
def formats
- @env["action_dispatch.request.formats"] ||=
- if parameters[:format]
+ @env["action_dispatch.request.formats"] ||= begin
+ params_readable = begin
+ parameters[:format]
+ rescue ActionController::BadRequest
+ false
+ end
+
+ if params_readable
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -64,8 +70,8 @@ module ActionDispatch
else
[Mime::HTML]
end
+ end
end
-
# Sets the \variant for template.
def variant=(variant)
if variant.is_a?(Symbol)
@@ -129,7 +135,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 3d2dd2d632..9450be838c 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -174,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
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index dcb299ed03..20ae48d458 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,13 +1,11 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/deprecation'
module ActionDispatch
module Http
module Parameters
- def initialize(env)
- super
- @symbolized_path_params = nil
- end
+ PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
# Returns both GET and POST \parameters in a single hash.
def parameters
@@ -18,55 +16,42 @@ module ActionDispatch
query_parameters.dup
end
params.merge!(path_parameters)
- params.with_indifferent_access
end
end
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- @symbolized_path_params = nil
- @env.delete("action_dispatch.request.parameters")
- @env["action_dispatch.request.path_parameters"] = parameters
+ @env.delete('action_dispatch.request.parameters')
+ @env[PARAMETERS_KEY] = parameters
end
- # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
def symbolized_path_parameters
- @symbolized_path_params ||= path_parameters.symbolize_keys
+ ActiveSupport::Deprecation.warn(
+ "`symbolized_path_parameters` is deprecated. Please use `path_parameters`"
+ )
+ path_parameters
end
# Returns a hash with the \parameters used to form the \path of the request.
# Returned hash keys are strings:
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
- #
- # See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
- @env["action_dispatch.request.path_parameters"] ||= {}
- end
-
- def reset_parameters #:nodoc:
- @env.delete("action_dispatch.request.parameters")
+ @env[PARAMETERS_KEY] ||= {}
end
private
- # Convert nested Hash to HashWithIndifferentAccess
- # and UTF-8 encode both keys and values in nested Hash.
+ # Convert nested Hash to HashWithIndifferentAccess.
#
- # TODO: Validate that the characters are UTF-8. If they aren't,
- # you'll get a weird error down the road, but our form handling
- # should really prevent that from happening
def normalize_encode_params(params)
case params
- when String
- params.force_encoding(Encoding::UTF_8).encode!
when Hash
if params.has_key?(:tempfile)
UploadedFile.new(params)
else
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)
+ new_hash[key] = if val.is_a?(Array)
val.map! { |el| normalize_encode_params(el) }
else
normalize_encode_params(val)
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 1318c62fbe..8c035c3c6c 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -23,7 +23,7 @@ module ActionDispatch
autoload :Session, 'action_dispatch/request/session'
autoload :Utils, 'action_dispatch/request/utils'
- LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
+ LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
@@ -53,6 +53,17 @@ module ActionDispatch
@uuid = nil
end
+ def check_path_parameters!
+ # If any of the path parameters has an invalid encoding then
+ # raise since it's likely to trigger errors further on.
+ path_parameters.each do |key, value|
+ next unless value.respond_to?(:valid_encoding?)
+ unless value.valid_encoding?
+ raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ end
+ end
+ end
+
def key?(key)
@env.key?(key)
end
@@ -64,6 +75,7 @@ module ActionDispatch
# Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt)
# Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt)
# Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt)
+ # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt)
# PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt)
RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
@@ -71,9 +83,10 @@ module ActionDispatch
RFC3648 = %w(ORDERPATCH)
RFC3744 = %w(ACL)
RFC5323 = %w(SEARCH)
+ RFC4791 = %w(MKCALENDAR)
RFC5789 = %w(PATCH)
- HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789
+ HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
HTTP_METHOD_LOOKUP = {}
@@ -152,6 +165,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
@@ -189,8 +209,8 @@ module ActionDispatch
end
# Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
- # (case-insensitive). All major JavaScript libraries send this header with
- # every Ajax request.
+ # (case-insensitive), which may need to be manually added depending on the
+ # choice of JavaScript libraries and frameworks.
def xml_http_request?
@env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
end
@@ -205,7 +225,7 @@ module ActionDispatch
@remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
end
- # Returns the unique request id, which is based off either the X-Request-Id header that can
+ # Returns the unique request id, which is based on either the X-Request-Id header that can
# be generated by a firewall, load balancer, or web server or by the RequestId middleware
# (which sets the action_dispatch.request_id environment variable).
#
@@ -271,7 +291,7 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((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
@@ -279,7 +299,7 @@ module ActionDispatch
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((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
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 2c6bcf7b7b..2fab6be1a5 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -91,7 +91,13 @@ module ActionDispatch # :nodoc:
end
def each(&block)
- @buf.each(&block)
+ @response.sending!
+ x = @buf.each(&block)
+ @response.sent!
+ x
+ end
+
+ def abort
end
def close
@@ -118,6 +124,8 @@ module ActionDispatch # :nodoc:
@blank = false
@cv = new_cond
@committed = false
+ @sending = false
+ @sent = false
@content_type = nil
@charset = nil
@@ -138,17 +146,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)
@@ -182,18 +210,6 @@ module ActionDispatch # :nodoc:
end
alias_method :status_message, :message
- def respond_to?(method, include_private = false)
- if method.to_s == 'to_path'
- stream.respond_to?(method)
- else
- super
- end
- end
-
- def to_path
- stream.to_path
- end
-
# Returns the content of the response as a string. This contains the contents
# of any calls to <tt>render</tt>.
def body
@@ -246,6 +262,17 @@ module ActionDispatch # :nodoc:
stream.close if stream.respond_to?(:close)
end
+ def abort
+ if stream.respond_to?(:abort)
+ stream.abort
+ elsif stream.respond_to?(:close)
+ # `stream.close` should really be reserved for a close from the
+ # other direction, but we must fall back to it for
+ # compatibility.
+ stream.close
+ end
+ end
+
# Turns the Response into a Rack-compatible array of the status, headers,
# and body.
def to_a
@@ -271,8 +298,17 @@ module ActionDispatch # :nodoc:
cookies
end
+ def _status_code
+ @status
+ end
private
+ def before_committed
+ end
+
+ def before_sending
+ end
+
def merge_default_headers(original, default)
return original unless default.respond_to?(:merge)
@@ -303,6 +339,38 @@ module ActionDispatch # :nodoc:
!@sending_file && @charset != false
end
+ class RackBody
+ def initialize(response)
+ @response = response
+ end
+
+ def each(*args, &block)
+ @response.each(*args, &block)
+ end
+
+ def close
+ # Rack "close" maps to Response#abort, and *not* Response#close
+ # (which is used when the controller's finished writing)
+ @response.abort
+ end
+
+ def body
+ @response.body
+ end
+
+ def respond_to?(method, include_private = false)
+ if method.to_s == 'to_path'
+ @response.stream.respond_to?(method)
+ else
+ super
+ end
+ end
+
+ def to_path
+ @response.stream.to_path
+ end
+ end
+
def rack_response(status, header)
assign_default_content_type_and_charset!(header)
handle_conditional_get!
@@ -313,7 +381,7 @@ module ActionDispatch # :nodoc:
header.delete CONTENT_TYPE
[status, header, []]
else
- [status, header, Rack::BodyProxy.new(self){}]
+ [status, header, RackBody.new(self)]
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index a8d2dc3950..540e11a4a0 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
@@ -26,7 +27,8 @@ module ActionDispatch
@tempfile = hash[:tempfile]
raise(ArgumentError, ':tempfile is required') unless @tempfile
- @original_filename = encode_filename(hash[:filename])
+ @original_filename = hash[:filename]
+ @original_filename &&= @original_filename.encode "UTF-8"
@content_type = hash[:type]
@headers = hash[:head]
end
@@ -65,13 +67,6 @@ module ActionDispatch
def eof?
@tempfile.eof?
end
-
- private
-
- def encode_filename(filename)
- # Encode the filename in the utf8 encoding, unless it is nil
- filename.force_encoding(Encoding::UTF_8).encode! if filename
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 6f5a52c568..6b8dcaf497 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -5,99 +5,121 @@ module ActionDispatch
module Http
module URL
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
- HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/
+ HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
mattr_accessor :tld_length
self.tld_length = 1
class << self
- def extract_domain(host, tld_length = @@tld_length)
- host.split('.').last(1 + tld_length).join('.') if named_host?(host)
+ def extract_domain(host, tld_length)
+ extract_domain_from(host, tld_length) if named_host?(host)
end
- def extract_subdomains(host, tld_length = @@tld_length)
+ def extract_subdomains(host, tld_length)
if named_host?(host)
- parts = host.split('.')
- parts[0..-(tld_length + 2)]
+ extract_subdomains_from(host, tld_length)
else
[]
end
end
- def extract_subdomain(host, tld_length = @@tld_length)
+ def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join('.')
end
- def url_for(options = {})
- options = options.dup
- path = options.delete(:script_name).to_s.chomp("/")
- path << options.delete(:path).to_s
-
- params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params)
- params.reject! { |_,v| v.to_param.nil? }
-
- result = build_host_url(options)
- if options[:trailing_slash]
- if path.include?('?')
- result << path.sub(/\?/, '/\&')
- else
- result << path.sub(/[^\/]\z|\A\z/, '\&/')
- end
+ def url_for(options)
+ if options[:only_path]
+ path_for options
else
- result << path
+ full_url_for options
end
- result << "?#{params.to_query}" unless params.empty?
- result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
- result
end
- private
+ def full_url_for(options)
+ host = options[:host]
+ protocol = options[:protocol]
+ port = options[:port]
- def build_host_url(options)
- if options[:host].blank? && options[:only_path].blank?
+ unless host
raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
end
- result = ""
+ build_host_url(host, port, protocol, options, path_for(options))
+ end
- unless options[:only_path]
- if match = options[:host].match(HOST_REGEXP)
- options[:protocol] ||= match[1] unless options[:protocol] == false
- options[:host] = match[2]
- options[:port] = match[3] unless options.key?(:port)
- end
+ def path_for(options)
+ path = options[:script_name].to_s.chomp("/")
+ path << options[:path] if options.key?(:path)
- options[:protocol] = normalize_protocol(options)
- options[:host] = normalize_host(options)
- options[:port] = normalize_port(options)
+ add_trailing_slash(path) if options[:trailing_slash]
+ add_params(path, options[:params]) if options.key?(:params)
+ add_anchor(path, options[:anchor]) if options.key?(:anchor)
- result << options[:protocol]
- result << rewrite_authentication(options)
- result << options[:host]
- result << ":#{options[:port]}" if options[:port]
- end
- result
+ path
end
- def named_host?(host)
- host && IP_HOST_REGEXP !~ host
+ private
+
+ def add_params(path, params)
+ params = { params: params } unless params.is_a?(Hash)
+ params.reject! { |_,v| v.to_param.nil? }
+ path << "?#{params.to_query}" unless params.empty?
+ end
+
+ def add_anchor(path, anchor)
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param.to_s)}"
+ end
+
+ def extract_domain_from(host, tld_length)
+ host.split('.').last(1 + tld_length).join('.')
end
- def same_host?(options)
- (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil?
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split('.')
+ parts[0..-(tld_length + 2)]
end
- def rewrite_authentication(options)
+ def add_trailing_slash(path)
+ # includes querysting
+ if path.include?('?')
+ path.sub!(/\?/, '/\&')
+ # does not have a .format
+ elsif !path.include?(".")
+ path.sub!(/[^\/]\z|\A\z/, '\&/')
+ end
+ end
+
+ def build_host_url(host, port, protocol, options, path)
+ if match = host.match(HOST_REGEXP)
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
+ end
+
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
+
+ result = protocol.dup
+
if options[:user] && options[:password]
- "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
- else
- ""
+ result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
end
+
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
+
+ result.concat path
end
- def normalize_protocol(options)
- case options[:protocol]
+ def named_host?(host)
+ IP_HOST_REGEXP !~ host
+ end
+
+ def normalize_protocol(protocol)
+ case protocol
when nil
"http://"
when false, "//"
@@ -105,36 +127,39 @@ module ActionDispatch
when PROTOCOL_REGEXP
"#{$1}://"
else
- raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}"
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
end
end
- def normalize_host(options)
- return options[:host] if !named_host?(options[:host]) || same_host?(options)
+ def normalize_host(_host, options)
+ return _host unless named_host?(_host)
tld_length = options[:tld_length] || @@tld_length
+ subdomain = options.fetch :subdomain, true
+ domain = options[:domain]
host = ""
- if options[:subdomain] == true || !options.key?(:subdomain)
- host << extract_subdomain(options[:host], tld_length).to_param
- elsif options[:subdomain].present?
- host << options[:subdomain].to_param
+ if subdomain == true
+ return _host if domain.nil?
+
+ host << extract_subdomains_from(_host, tld_length).join('.')
+ elsif subdomain
+ host << subdomain.to_param
end
host << "." unless host.empty?
- host << (options[:domain] || extract_domain(options[:host], tld_length))
+ host << (domain || extract_domain_from(_host, tld_length))
host
end
- def normalize_port(options)
- return nil if options[:port].nil? || options[:port] == false
+ def normalize_port(port, protocol)
+ return unless port
- case options[:protocol]
- when "//"
- nil
+ case protocol
+ when "//" then yield port
when "https://"
- options[:port].to_i == 443 ? nil : options[:port]
+ yield port unless port.to_i == 443
else
- options[:port].to_i == 80 ? nil : options[:port]
+ yield port unless port.to_i == 80
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 4410c1b5d5..59b353b1b7 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -12,12 +12,12 @@ module ActionDispatch
@cache = nil
end
- def generate(type, name, options, recall = {}, parameterize = nil)
- constraints = recall.merge(options)
+ def generate(name, options, path_parameters, parameterize = nil)
+ constraints = path_parameters.merge(options)
missing_keys = []
match_route(name, constraints) do |route|
- parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize)
+ parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
# Skip this route unless a name has been provided or it is a
# standard Rails route since we can't determine whether an options
@@ -30,6 +30,12 @@ module ActionDispatch
parameterized_parts.key?(key) || route.defaults.key?(key)
end
+ defaults = route.defaults
+ required_parts = route.required_parts
+ parameterized_parts.delete_if do |key, value|
+ value.to_s == defaults[key].to_s && !required_parts.include?(key)
+ end
+
return [route.format(parameterized_parts), params]
end
@@ -74,12 +80,12 @@ module ActionDispatch
if named_routes.key?(name)
yield named_routes[name]
else
- routes = non_recursive(cache, options.to_a)
+ routes = non_recursive(cache, options)
hash = routes.group_by { |_, r| r.score(options) }
hash.keys.sort.reverse_each do |score|
- next if score < 0
+ break if score < 0
hash[score].sort_by { |i, _| i }.each do |_, route|
yield route
@@ -90,14 +96,14 @@ module ActionDispatch
def non_recursive(cache, options)
routes = []
- stack = [cache]
+ queue = [cache]
- while stack.any?
- c = stack.shift
+ while queue.any?
+ c = queue.shift
routes.concat(c[:___routes]) if c.key?(:___routes)
options.each do |pair|
- stack << c[pair] if c.key?(pair)
+ queue << c[pair] if c.key?(pair)
end
end
@@ -121,14 +127,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+.
- def verify_required_parts!(route, parts)
- missing_keys(route, parts).empty?
+ }
end
def build_cache
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 5a79059ed6..990d2127ee 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -40,7 +40,19 @@ module ActionDispatch
end
def move(t, a)
- move_string(t, a).concat(move_regexp(t, a))
+ return [] if t.empty?
+
+ regexps = []
+
+ 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)
@@ -114,17 +126,17 @@ module ActionDispatch
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
@@ -139,26 +151,6 @@ module ActionDispatch
raise ArgumentError, 'unknown symbol: %s' % sym.class
end
end
-
- def move_regexp(t, a)
- return [] if t.empty?
-
- t.map { |s|
- if states = @regexp_states[s]
- states.map { |re, v| re === a ? v : nil }
- end
- }.flatten.compact.uniq
- end
-
- def move_string(t, a)
- return [] if t.empty?
-
- t.map do |s|
- if states = @string_states[s]
- states[a]
- end
- end.compact
- 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/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index 935442ef66..bb01c087bc 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -93,6 +93,10 @@ module ActionDispatch
class Star < Unary # :nodoc:
def type; :STAR; end
+
+ def name
+ left.name.tr '*:', ''
+ end
end
class Binary < Node # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index 430812fafe..9012297400 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 grammar file "".
+# This file is automatically generated by Racc 1.4.11
+# from Racc grammer file "".
#
require 'racc/parser.rb'
@@ -9,42 +9,38 @@ require 'racc/parser.rb'
require 'action_dispatch/journey/parser_extras'
module ActionDispatch
- module Journey # :nodoc:
- class Parser < Racc::Parser # :nodoc:
+ module Journey
+ class Parser < Racc::Parser
##### State transition tables begin ###
racc_action_table = [
- 17, 21, 13, 15, 14, 7, nil, 16, 8, 19,
- 13, 15, 14, 7, 23, 16, 8, 19, 13, 15,
- 14, 7, nil, 16, 8, 13, 15, 14, 7, nil,
- 16, 8, 13, 15, 14, 7, nil, 16, 8 ]
+ 13, 15, 14, 7, 21, 16, 8, 19, 13, 15,
+ 14, 7, 17, 16, 8, 13, 15, 14, 7, 24,
+ 16, 8, 13, 15, 14, 7, 19, 16, 8 ]
racc_action_check = [
- 1, 17, 1, 1, 1, 1, nil, 1, 1, 1,
- 20, 20, 20, 20, 20, 20, 20, 20, 7, 7,
- 7, 7, nil, 7, 7, 19, 19, 19, 19, nil,
- 19, 19, 0, 0, 0, 0, nil, 0, 0 ]
+ 2, 2, 2, 2, 17, 2, 2, 2, 0, 0,
+ 0, 0, 1, 0, 0, 19, 19, 19, 19, 20,
+ 19, 19, 7, 7, 7, 7, 22, 7, 7 ]
racc_action_pointer = [
- 30, 0, nil, nil, nil, nil, nil, 16, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, 1, nil, 23,
- 8, nil, nil, nil ]
+ 6, 12, -2, nil, nil, nil, nil, 20, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, 4, nil, 13,
+ 13, nil, 17, nil, nil ]
racc_action_default = [
- -18, -18, -2, -3, -4, -5, -6, -18, -9, -10,
- -11, -12, -13, -14, -15, -16, -17, -18, -1, -18,
- -18, 24, -8, -7 ]
+ -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
+ -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
+ -19, 25, -8, -9, -7 ]
racc_goto_table = [
- 18, 1, nil, nil, nil, nil, nil, nil, 20, nil,
- nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ]
+ 1, 22, 18, 23, nil, nil, nil, 20 ]
racc_goto_check = [
- 2, 1, nil, nil, nil, nil, nil, nil, 1, nil,
- nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ]
+ 1, 2, 1, 3, nil, nil, nil, 1 ]
racc_goto_pointer = [
- nil, 1, -1, nil, nil, nil, nil, nil, nil, nil,
+ nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
nil ]
racc_goto_default = [
@@ -61,19 +57,20 @@ racc_reduce_table = [
1, 12, :_reduce_none,
3, 15, :_reduce_7,
3, 13, :_reduce_8,
- 1, 16, :_reduce_9,
+ 3, 13, :_reduce_9,
+ 1, 16, :_reduce_10,
1, 14, :_reduce_none,
1, 14, :_reduce_none,
1, 14, :_reduce_none,
1, 14, :_reduce_none,
- 1, 19, :_reduce_14,
- 1, 17, :_reduce_15,
- 1, 18, :_reduce_16,
- 1, 20, :_reduce_17 ]
+ 1, 19, :_reduce_15,
+ 1, 17, :_reduce_16,
+ 1, 18, :_reduce_17,
+ 1, 20, :_reduce_18 ]
-racc_reduce_n = 18
+racc_reduce_n = 19
-racc_shift_n = 24
+racc_shift_n = 25
racc_token_table = {
false => 0,
@@ -89,7 +86,7 @@ racc_token_table = {
racc_nt_base = 10
-racc_use_result_var = true
+racc_use_result_var = false
Racc_arg = [
racc_action_table,
@@ -136,14 +133,12 @@ Racc_debug_parser = false
# reduce 0 omitted
-def _reduce_1(val, _values, result)
- result = Cat.new(val.first, val.last)
- result
+def _reduce_1(val, _values)
+ Cat.new(val.first, val.last)
end
-def _reduce_2(val, _values, result)
- result = val.first
- result
+def _reduce_2(val, _values)
+ val.first
end
# reduce 3 omitted
@@ -154,22 +149,21 @@ end
# reduce 6 omitted
-def _reduce_7(val, _values, result)
- result = Group.new(val[1])
- result
+def _reduce_7(val, _values)
+ Group.new(val[1])
end
-def _reduce_8(val, _values, result)
- result = Or.new([val.first, val.last])
- result
+def _reduce_8(val, _values)
+ Or.new([val.first, val.last])
end
-def _reduce_9(val, _values, result)
- result = Star.new(Symbol.new(val.last))
- result
+def _reduce_9(val, _values)
+ Or.new([val.first, val.last])
end
-# reduce 10 omitted
+def _reduce_10(val, _values)
+ Star.new(Symbol.new(val.last))
+end
# reduce 11 omitted
@@ -177,27 +171,25 @@ end
# reduce 13 omitted
-def _reduce_14(val, _values, result)
- result = Slash.new('/')
- result
+# reduce 14 omitted
+
+def _reduce_15(val, _values)
+ Slash.new('/')
end
-def _reduce_15(val, _values, result)
- result = Symbol.new(val.first)
- result
+def _reduce_16(val, _values)
+ Symbol.new(val.first)
end
-def _reduce_16(val, _values, result)
- result = Literal.new(val.first)
- result
+def _reduce_17(val, _values)
+ Literal.new(val.first)
end
-def _reduce_17(val, _values, result)
- result = Dot.new(val.first)
- result
+def _reduce_18(val, _values)
+ Dot.new(val.first)
end
-def _reduce_none(val, _values, result)
+def _reduce_none(val, _values)
val[0]
end
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
index 040f8d5922..d3f7c4d765 100644
--- a/actionpack/lib/action_dispatch/journey/parser.y
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -1,11 +1,11 @@
class ActionDispatch::Journey::Parser
-
+ options no_result_var
token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
rule
expressions
- : expressions expression { result = Cat.new(val.first, val.last) }
- | expression { result = val.first }
+ : expression expressions { Cat.new(val.first, val.last) }
+ | expression { val.first }
| or
;
expression
@@ -14,13 +14,14 @@ rule
| star
;
group
- : LPAREN expressions RPAREN { result = Group.new(val[1]) }
+ : LPAREN expressions RPAREN { Group.new(val[1]) }
;
or
- : expressions OR expression { result = Or.new([val.first, val.last]) }
+ : expression OR expression { Or.new([val.first, val.last]) }
+ | expression OR or { Or.new([val.first, val.last]) }
;
star
- : STAR { result = Star.new(Symbol.new(val.last)) }
+ : STAR { Star.new(Symbol.new(val.last)) }
;
terminal
: symbol
@@ -29,16 +30,16 @@ rule
| dot
;
slash
- : SLASH { result = Slash.new('/') }
+ : SLASH { Slash.new('/') }
;
symbol
- : SYMBOL { result = Symbol.new(val.first) }
+ : SYMBOL { Symbol.new(val.first) }
;
literal
- : LITERAL { result = Literal.new(val.first) }
+ : LITERAL { Literal.new(val.first) }
;
dot
- : DOT { result = Dot.new(val.first) }
+ : DOT { Dot.new(val.first) }
;
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index d37aa1fbe5..3af940a02f 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -1,27 +1,20 @@
+require 'action_dispatch/journey/router/strexp'
+
module ActionDispatch
module Journey # :nodoc:
module Path # :nodoc:
class Pattern # :nodoc:
attr_reader :spec, :requirements, :anchored
- def initialize(strexp)
- parser = Journey::Parser.new
-
- @anchored = true
+ def self.from_string string
+ new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
+ end
- case strexp
- when String
- @spec = parser.parse(strexp)
- @requirements = {}
- @separators = "/.?"
- when Router::Strexp
- @spec = parser.parse(strexp.path)
- @requirements = strexp.requirements
- @separators = strexp.separators.join
- @anchored = strexp.anchor
- else
- raise ArgumentError, "Bad expression: #{strexp}"
- end
+ def initialize(strexp)
+ @spec = strexp.ast
+ @requirements = strexp.requirements
+ @separators = strexp.separators.join
+ @anchored = strexp.anchor
@names = nil
@optional_names = nil
@@ -30,6 +23,10 @@ module ActionDispatch
@offsets = nil
end
+ def build_formatter
+ Visitors::FormatBuilder.new.accept(spec)
+ end
+
def ast
@spec.grep(Nodes::Symbol).each do |node|
re = @requirements[node.to_sym]
@@ -53,9 +50,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 c8eb0f6f2d..9f0a3af902 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -16,14 +16,6 @@ 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
@@ -31,6 +23,7 @@ module ActionDispatch
@parts = nil
@decorated_ast = nil
@precedence = 0
+ @path_formatter = @path.build_formatter
end
def ast
@@ -72,15 +65,7 @@ module ActionDispatch
alias :segment_keys :parts
def format(path_options)
- path_options.delete_if do |key, value|
- value.to_s == defaults[key].to_s && !required_parts.include?(key)
- end
-
- Visitors::Formatter.new(path_options).accept(path.spec)
- end
-
- def optimized_path
- Visitors::OptimizedPath.new.accept(path.spec)
+ @path_formatter.evaluate path_options
end
def optional_parts
@@ -101,8 +86,12 @@ module ActionDispatch
end
end
+ def glob?
+ !path.spec.grep(Nodes::Star).empty?
+ end
+
def dispatcher?
- @dispatcher
+ @app.dispatcher?
end
def matches?(request)
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 419e665d12..21817b374c 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -20,60 +20,32 @@ module ActionDispatch
# :nodoc:
VERSION = '2.0.0'
- class NullReq # :nodoc:
- attr_reader :env
- def initialize(env)
- @env = env
- end
-
- def request_method
- env['REQUEST_METHOD']
- end
-
- def path_info
- env['PATH_INFO']
- end
-
- def ip
- env['REMOTE_ADDR']
- end
-
- def [](k)
- env[k]
- end
- end
-
- attr_reader :request_class, :formatter
attr_accessor :routes
- def initialize(routes, options)
- @options = options
- @params_key = options[:parameters_key]
- @request_class = options[:request_class] || NullReq
- @routes = routes
+ def initialize(routes)
+ @routes = routes
end
- def call(env)
- 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',
- 'PATH_INFO',
- @params_key)
+ def serve(req)
+ find_routes(req).each do |match, parameters, route|
+ set_params = req.path_parameters
+ path_info = req.path_info
+ script_name = req.script_name
unless route.path.anchored
- env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/')
- env['PATH_INFO'] = match.post_match
+ req.script_name = (script_name.to_s + match.to_s).chomp('/')
+ req.path_info = match.post_match
+ req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
end
- env[@params_key] = (set_params || {}).merge parameters
+ req.path_parameters = set_params.merge parameters
- status, headers, body = route.app.call(env)
+ status, headers, body = route.app.serve(req)
if 'pass' == headers['X-Cascade']
- env['SCRIPT_NAME'] = script_name
- env['PATH_INFO'] = path_info
- env[@params_key] = set_params
+ req.script_name = script_name
+ req.path_info = path_info
+ req.path_parameters = set_params
next
end
@@ -83,14 +55,14 @@ module ActionDispatch
return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
end
- def recognize(req)
- find_routes(req.env).each do |match, parameters, route|
+ def recognize(rails_req)
+ find_routes(rails_req).each do |match, parameters, route|
unless route.path.anchored
- req.env['SCRIPT_NAME'] = match.to_s
- req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1')
+ rails_req.script_name = match.to_s
+ rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
end
- yield(route, nil, parameters)
+ yield(route, parameters)
end
end
@@ -121,33 +93,34 @@ module ActionDispatch
def filter_routes(path)
return [] unless ast
- data = simulator.match(path)
- data ? data.memos : []
+ simulator.memos(path) { [] }
end
- def find_routes env
- req = request_class.new(env)
-
+ def find_routes req
routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
r.path.match(req.path_info)
}
- routes.concat get_routes_as_head(routes)
- routes.sort_by!(&:precedence).select! { |r| r.matches?(req) }
+ if req.env["REQUEST_METHOD"] === "HEAD"
+ routes.concat get_routes_as_head(routes)
+ end
+
+ routes.select! { |r| r.matches?(req) }
+ routes.sort_by!(&:precedence)
routes.map! { |r|
match_data = r.path.match(req.path_info)
- match_names = match_data.names.map { |n| n.to_sym }
- match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) }
- info = Hash[match_names.zip(match_values).find_all { |_, y| y }]
-
- [match_data, r.defaults.merge(info), r]
+ path_parameters = r.defaults.dup
+ match_data.names.zip(match_data.captures) { |name,val|
+ path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
+ }
+ [match_data, path_parameters, r]
}
end
def get_routes_as_head(routes)
precedence = (routes.map(&:precedence).max || 0) + 1
- routes = routes.select { |r|
+ routes.select { |r|
r.verb === "GET" && !(r.verb === "HEAD")
}.map! { |r|
Route.new(r.name,
@@ -158,8 +131,6 @@ module ActionDispatch
route.precedence = r.precedence + precedence
end
}
- routes.flatten!
- routes
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb
index f97f1a223e..4b7738f335 100644
--- a/actionpack/lib/action_dispatch/journey/router/strexp.rb
+++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb
@@ -6,18 +6,21 @@ module ActionDispatch
alias :compile :new
end
- attr_reader :path, :requirements, :separators, :anchor
+ attr_reader :path, :requirements, :separators, :anchor, :ast
- def initialize(path, requirements, separators, anchor = true)
+ def self.build(path, requirements, separators, anchor = true)
+ parser = Journey::Parser.new
+ ast = parser.parse path
+ new ast, path, requirements, separators, anchor
+ end
+
+ def initialize(ast, path, requirements, separators, anchor = true)
+ @ast = ast
@path = path
@requirements = requirements
@separators = separators
@anchor = anchor
end
-
- def names
- @path.scan(/:\w+/).map { |s| s.tr(':', '') }
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index d1a004af50..2b0a6575d4 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:
@@ -25,31 +23,69 @@ module ActionDispatch
# 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
+ US_ASCII = Encoding::US_ASCII
+ UTF_8 = Encoding::UTF_8
+ EMPTY = "".force_encoding(US_ASCII).freeze
+ DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) }
+
+ 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)
+ encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
+ uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding)
+ end
+
+ protected
+ def escape(component, pattern)
+ component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
+ end
+
+ def percent_encode(unsafe)
+ safe = EMPTY.dup
+ unsafe.each_byte { |b| safe << DEC2HEX[b] }
+ safe
+ end
end
- Parser = URI::Parser.new
+ 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/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index daade5bb74..52b4c8b489 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,14 +1,57 @@
# encoding: utf-8
-require 'thread_safe'
-
module ActionDispatch
module Journey # :nodoc:
+ class Format
+ ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
+ ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
+
+ class Parameter < Struct.new(:name, :escaper)
+ def escape(value); escaper.call value; end
+ end
+
+ def self.required_path(symbol)
+ Parameter.new symbol, ESCAPE_PATH
+ end
+
+ def self.required_segment(symbol)
+ Parameter.new symbol, ESCAPE_SEGMENT
+ end
+
+ def initialize(parts)
+ @parts = parts
+ @children = []
+ @parameters = []
+
+ parts.each_with_index do |object,i|
+ case object
+ when Journey::Format
+ @children << i
+ when Parameter
+ @parameters << i
+ end
+ end
+ end
+
+ def evaluate(hash)
+ parts = @parts.dup
+
+ @parameters.each do |index|
+ param = parts[index]
+ value = hash[param.name]
+ return ''.freeze unless value
+ parts[index] = param.escape value
+ end
+
+ @children.each { |index| parts[index] = parts[index].evaluate(hash) }
+
+ parts.join
+ end
+ end
+
module Visitors # :nodoc:
class Visitor # :nodoc:
- DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k|
- h[k] = :"visit_#{k}"
- }
+ DISPATCH_CACHE = {}
def accept(node)
visit(node)
@@ -38,11 +81,41 @@ module ActionDispatch
def visit_STAR(n); unary(n); end
def terminal(node); end
- %w{ LITERAL SYMBOL SLASH DOT }.each do |t|
- class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__
+ def visit_LITERAL(n); terminal(n); end
+ def visit_SYMBOL(n); terminal(n); end
+ def visit_SLASH(n); terminal(n); end
+ def visit_DOT(n); terminal(n); end
+
+ private_instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
end
end
+ class FormatBuilder < Visitor # :nodoc:
+ def accept(node); Journey::Format.new(super); end
+ def terminal(node); [node.left]; end
+
+ def binary(node)
+ visit(node.left) + visit(node.right)
+ end
+
+ def visit_GROUP(n); [Journey::Format.new(unary(n))]; end
+
+ def visit_STAR(n)
+ [Journey::Format.required_path(n.left.to_sym)]
+ end
+
+ def visit_SYMBOL(n)
+ symbol = n.to_sym
+ if symbol == :controller
+ [Journey::Format.required_path(symbol)]
+ else
+ [Journey::Format.required_segment(symbol)]
+ end
+ end
+ end
+
# Loop through the requirements AST
class Each < Visitor # :nodoc:
attr_reader :block
@@ -52,8 +125,8 @@ module ActionDispatch
end
def visit(node)
- super
block.call(node)
+ super
end
end
@@ -77,77 +150,6 @@ module ActionDispatch
end
end
- class OptimizedPath < Visitor # :nodoc:
- def accept(node)
- Array(visit(node))
- end
-
- private
-
- 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
-
- def initialize(options)
- @options = options
- end
-
- private
-
- def visit(node, optional = false)
- case node.type
- when :LITERAL, :SLASH, :DOT
- node.left
- when :STAR
- visit(node.left)
- when :GROUP
- visit(node.left, true)
- when :CAT
- visit_CAT(node, optional)
- when :SYMBOL
- visit_SYMBOL(node)
- end
- end
-
- def visit_CAT(node, optional)
- left = visit(node.left, optional)
- right = visit(node.right, optional)
-
- if optional && !(right && left)
- ""
- else
- [left, right].join
- end
- end
-
- def visit_SYMBOL(node)
- if value = options[node.to_sym]
- Router::Utils.escape_path(value)
- end
- end
- end
-
class Dot < Visitor # :nodoc:
def initialize
@nodes = []
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
index 6aff10956a..9b28a65200 100644
--- a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
+++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
@@ -2,13 +2,13 @@
<html>
<head>
<title><%= title %></title>
- <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css">
<style>
<% stylesheets.each do |style| %>
<%= style %>
<% end %>
</style>
- <script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script>
</head>
<body>
<div id="wrapper">
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 18e64704f6..ac9e5effe2 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -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.
@@ -176,11 +176,11 @@ 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|
+ deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
self[name] = { value: value }
end
rescue ActiveSupport::MessageVerifier::InvalidSignature
@@ -237,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)
@@ -280,8 +289,8 @@ module ActionDispatch
end
end
- # Sets the cookie named +name+. The second argument may be the very cookie
- # value, or a hash of options as documented above.
+ # Sets the cookie named +name+. The second argument may be the cookie's
+ # value or a hash of options as documented above.
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
@@ -336,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
@@ -459,7 +468,7 @@ module ActionDispatch
options = { :value => @verifier.generate(serialize(name, options)) }
end
- raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
@parent_jar[name] = options
end
@@ -517,7 +526,7 @@ module ActionDispatch
options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
- raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
@parent_jar[name] = options
end
@@ -551,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 0ca1a87645..274f6f2f22 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -38,9 +38,7 @@ module ActionDispatch
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
request: request,
exception: wrapper.exception,
- application_trace: wrapper.application_trace,
- framework_trace: wrapper.framework_trace,
- full_trace: wrapper.full_trace,
+ traces: traces_from_wrapper(wrapper),
routes_inspector: routes_inspector(exception),
source_extract: wrapper.source_extract,
line_number: wrapper.line_number,
@@ -95,5 +93,36 @@ module ActionDispatch
ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
end
end
+
+ # Augment the exception traces by providing ids for all unique stack frame
+ def traces_from_wrapper(wrapper)
+ application_trace = wrapper.application_trace
+ framework_trace = wrapper.framework_trace
+ full_trace = wrapper.full_trace
+
+ if application_trace && framework_trace
+ id_counter = 0
+
+ application_trace = application_trace.map do |trace|
+ prev = id_counter
+ id_counter += 1
+ { id: prev, trace: trace }
+ end
+
+ framework_trace = framework_trace.map do |trace|
+ prev = id_counter
+ id_counter += 1
+ { id: prev, trace: trace }
+ end
+
+ full_trace = application_trace + framework_trace
+ end
+
+ {
+ "Application Trace" => application_trace,
+ "Framework Trace" => framework_trace,
+ "Full Trace" => full_trace
+ }
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 377f05c982..b98b553c38 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -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
@@ -59,12 +61,15 @@ module ActionDispatch
end
def source_extract
- if application_trace && trace = application_trace.first
- file, line, _ = trace.split(":")
- @file = file
- @line_number = line.to_i
- source_fragment(@file, @line_number)
- end
+ exception.backtrace.map do |trace|
+ file, line = trace.split(":")
+ line_number = line.to_i
+ {
+ code: source_fragment(file, line_number),
+ file: file,
+ line_number: line_number
+ }
+ end if exception.backtrace
end
private
@@ -104,5 +109,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 4821d2a899..e90f8b9ce6 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -10,7 +10,7 @@ module ActionDispatch
end
end
- # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
+ # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done.
@@ -37,8 +37,11 @@ module ActionDispatch
# flash.alert = "You must be logged in"
# flash.notice = "Post successfully created"
#
- # This example just places a string in the flash, but you can put any object in there. And of course, you can put as
- # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
+ # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
+ # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
+ # use sanitize helper.
+ #
+ # Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
class Flash
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index cbb2d475b1..6c8944e067 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -32,9 +32,8 @@ module ActionDispatch
end
def render_html(status)
- found = false
- path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
- path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, 'text/html', File.read(path))
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 57bc6d5cd0..6a79b4e859 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -11,7 +11,7 @@ module ActionDispatch
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
# requires. Some Rack servers simply drop preceding headers, and only report
# the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
- # If you are behind multiple proxy servers (like Nginx to HAProxy to Unicorn)
+ # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
# then you should test your Rack server to make sure your data is good.
#
# IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
@@ -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
@@ -118,7 +118,7 @@ module ActionDispatch
#
# REMOTE_ADDR will be correct if the request is made directly against the
# Ruby process, on e.g. Heroku. When the request is proxied by another
- # server like HAProxy or Nginx, the IP address that made the original
+ # server like HAProxy or NGINX, the IP address that made the original
# request will be put in an X-Forwarded-For header. If there are multiple
# proxies, that header may contain a list of IPs. Other proxy services
# set the Client-Ip header instead, so we check that too.
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 5d1740d0d4..25658bac3d 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -5,7 +5,7 @@ module ActionDispatch
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
# ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
#
- # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
#
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 1db6194271..625050dc4b 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -16,9 +16,9 @@ module ActionDispatch
# Get a session from the cache.
def get_session(env, sid)
- sid ||= generate_sid
- session = @cache.read(cache_key(sid))
- session ||= {}
+ unless sid and session = @cache.read(cache_key(sid))
+ sid, session = generate_sid, {}
+ end
[sid, session]
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 1ebc189c28..ed25c67ae5 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -29,7 +29,7 @@ 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/secrets.yml:
#
@@ -49,9 +49,9 @@ module ActionDispatch
# reasonably sure that your upgrade is otherwise complete. Additionally,
# you should 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.
+ # 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 1d4f0f89a6..f0779279c1 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -42,6 +42,7 @@ module ActionDispatch
wrapper = ExceptionWrapper.new(env, exception)
status = wrapper.status_code
env["action_dispatch.exception"] = wrapper.exception
+ env["action_dispatch.original_path"] = env["PATH_INFO"]
env["PATH_INFO"] = "/#{status}"
response = @exceptions_app.call(env)
response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 999c022535..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
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
index 38429cb78e..51660a619b 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
@@ -1,25 +1,29 @@
<% if @source_extract %>
-<div class="source">
-<div class="info">
- Extracted source (around line <strong>#<%= @line_number %></strong>):
-</div>
-<div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% @source_extract.keys.each do |line_number| %>
+ <% @source_extract.each_with_index do |extract_source, index| %>
+ <% if extract_source[:code] %>
+ <div class="source <%="hidden" if index != 0%>" id="frame-source-<%=index%>">
+ <div class="info">
+ Extracted source (around line <strong>#<%= extract_source[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% extract_source[:code].keys.each do |line_number| %>
<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
+ <% end %>
+ </pre>
+ </td>
<td width="100%">
<pre>
-<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @line_number -%>"><%= source -%></div><% end -%>
+<% extract_source[:code].each do |line, source| -%><div class="line<%= " active" if line == extract_source[:line_number] -%>"><%= source -%></div><% end -%>
</pre>
</td>
- </tr>
- </table>
-</div>
-</div>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
+ <% end %>
<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
index b181909bff..f62caf51d7 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -1,9 +1,4 @@
-<%
- traces = { "Application Trace" => @application_trace,
- "Framework Trace" => @framework_trace,
- "Full Trace" => @full_trace }
- names = traces.keys
-%>
+<% names = @traces.keys %>
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
@@ -16,9 +11,42 @@
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
<% end %>
- <% traces.each do |name, trace| %>
+ <% @traces.each do |name, trace| %>
<div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;">
- <pre><code><%= trace.join "\n" %></code></pre>
+ <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
</div>
<% end %>
+
+ <script type="text/javascript">
+ var traceFrames = document.getElementsByClassName('trace-frames');
+ var selectedFrame, currentSource = document.getElementById('frame-source-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
+ }
+ }
+ </script>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
index d4af5c9b06..c0b53068f7 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -1,15 +1,9 @@
-<%
- 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| %>
+<% @traces.each do |name, trace| %>
<% if trace.any? %>
<%= name %>
-<%= trace.join("\n") %>
+<%= trace.map { |t| t[: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 f154021ae6..f154021ae6 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
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/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index bc5d03dc10..e0509f56f4 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -116,9 +116,15 @@
background-color: #FFCCCC;
}
+ .hidden {
+ display: none;
+ }
+
a { color: #980905; }
a:visited { color: #666; }
+ a.trace-frames { color: #666; }
a:hover { color: #C52F24; }
+ a.trace-frames.selected { color: #C52F24 }
<%= yield :style %>
</style>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
index 027a0f5b3e..c1e8b6cae3 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -1,4 +1,3 @@
-<% @source_extract = @exception.source_extract(0, :html) %>
<header>
<h1>
<%= @exception.original_exception.class.to_s %> in
@@ -12,29 +11,7 @@
</p>
<pre><code><%= h @exception.message %></code></pre>
- <div class="source">
- <div class="info">
- <p>Extracted source (around line <strong>#<%= @exception.line_number %></strong>):</p>
- </div>
- <div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% @source_extract.keys.each do |line_number| %>
-<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
-<td width="100%">
-<pre>
-<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @exception.line_number -%>"><%= source -%></div><% end -%>
-</pre>
-</td>
- </tr>
- </table>
-</div>
-</div>
+ <%= render template: "rescues/_source" %>
<p><%= @exception.sub_template_message %></p>
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
index 5da21d9784..77bcd26726 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -1,4 +1,3 @@
-<% @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:
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 323873ba4b..6ffa242da4 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 parent <tr> into the callback function
- // if the regexp matches 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 (' + escape(sanitizePath(userInput)) +'):</th></tr>',
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(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/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index a9ac2bce1d..ce03164ca9 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,6 +1,7 @@
# encoding: UTF-8
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/regexp'
+require 'active_support/dependencies/autoload'
module ActionDispatch
# The routing module provides URL rewriting in native Ruby. It's a way to
@@ -11,7 +12,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/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
new file mode 100644
index 0000000000..88aa13c3e8
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/endpoint.rb
@@ -0,0 +1,10 @@
+module ActionDispatch
+ module Routing
+ class Endpoint # :nodoc:
+ def dispatcher?; false; end
+ def redirect?; false; end
+ def matches?(req); true; end
+ def app; self; end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index 71a0c5e826..ea3b2f419d 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -5,22 +5,15 @@ module ActionDispatch
module Routing
class RouteWrapper < SimpleDelegator
def endpoint
- rack_app ? rack_app.inspect : "#{controller}##{action}"
+ app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
end
def constraints
requirements.except(:controller, :action)
end
- def rack_app(app = self.app)
- @rack_app ||= begin
- class_name = app.class.name.to_s
- if class_name == "ActionDispatch::Routing::Mapper::Constraints"
- rack_app(app.app)
- elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
- app
- end
- end
+ def rack_app
+ app.app
end
def verb
@@ -73,7 +66,7 @@ module ActionDispatch
end
def engine?
- rack_app && rack_app.respond_to?(:routes)
+ rack_app.respond_to?(:routes)
end
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 0b762aa9a4..e92baa5aa7 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -6,130 +6,198 @@ 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'
+require 'action_dispatch/routing/endpoint'
+require 'active_support/deprecation'
module ActionDispatch
module Routing
class Mapper
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
- SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
- :controller, :action, :path_names, :constraints,
- :shallow, :blocks, :defaults, :options]
-
- class Constraints #:nodoc:
- def self.new(app, constraints, request = Rack::Request)
- if constraints.any?
- super(app, constraints, request)
- else
- app
- end
- end
+ class Constraints < Endpoint #:nodoc:
attr_reader :app, :constraints
- def initialize(app, constraints, request)
- @app, @constraints, @request = app, constraints, request
+ def initialize(app, constraints, dispatcher_p)
+ # Unwrap Constraints objects. I don't actually think it's possible
+ # to pass a Constraints object to this constructor, but there were
+ # multiple places that kept testing children of this object. I
+ # *think* they were just being defensive, but I have no idea.
+ if app.is_a?(self.class)
+ constraints += app.constraints
+ app = app.app
+ end
+
+ @dispatcher = dispatcher_p
+
+ @app, @constraints, = app, constraints
end
- def matches?(env)
- req = @request.new(env)
+ def dispatcher?; @dispatcher; end
+ def matches?(req)
@constraints.all? do |constraint|
(constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
(constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
end
- ensure
- req.reset_parameters
end
- def call(env)
- matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
+ def serve(req)
+ return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
+
+ if dispatcher?
+ @app.serve req
+ else
+ @app.call req.env
+ end
end
private
def constraint_args(constraint, request)
- constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
end
end
class Mapping #:nodoc:
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
- attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
+ attr_reader :requirements, :conditions, :defaults
+ attr_reader :to, :default_controller, :default_action, :as, :anchor
+
+ def self.build(scope, set, path, as, options)
+ options = scope[:options].merge(options) if scope[:options]
+
+ options.delete :only
+ options.delete :except
+ options.delete :shallow_path
+ options.delete :shallow_prefix
+ options.delete :shallow
+
+ defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
+
+ new scope, set, path, defaults, as, options
+ end
+
+ def initialize(scope, set, path, defaults, as, options)
+ @requirements, @conditions = {}, {}
+ @defaults = defaults
+ @set = set
+
+ @to = options.delete :to
+ @default_controller = options.delete(:controller) || scope[:controller]
+ @default_action = options.delete(:action) || scope[:action]
+ @as = as
+ @anchor = options.delete :anchor
+
+ formatted = options.delete :format
+ via = Array(options.delete(:via) { [] })
+ options_constraints = options.delete :constraints
+
+ path = normalize_path! path, formatted
+ ast = path_ast path
+ path_params = path_params ast
+
+ options = normalize_options!(options, formatted, path_params, ast, scope[:module])
+
+
+ split_constraints(path_params, scope[:constraints]) if scope[:constraints]
+ constraints = constraints(options, path_params)
+
+ split_constraints path_params, constraints
+
+ @blocks = blocks(options_constraints, scope[:blocks])
- def initialize(set, scope, path, options)
- @set, @scope, @path, @options = set, scope, path, options
- @requirements, @conditions, @defaults = {}, {}, {}
+ if options_constraints.is_a?(Hash)
+ split_constraints path_params, options_constraints
+ options_constraints.each do |key, default|
+ if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ @defaults[key] ||= default
+ end
+ end
+ end
- normalize_options!
- normalize_path!
- normalize_requirements!
- normalize_conditions!
- normalize_defaults!
+ normalize_format!(formatted)
+
+ @conditions[:path_info] = path
+ @conditions[:parsed_path_info] = ast
+
+ add_request_method(via, @conditions)
+ normalize_defaults!(options)
end
def to_route
- [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
+ [ app(@blocks), conditions, requirements, defaults, as, anchor ]
end
private
- def normalize_path!
- raise ArgumentError, "path is required" if @path.blank?
- @path = Mapper.normalize_path(@path)
+ def normalize_path!(path, format)
+ path = Mapper.normalize_path(path)
- if required_format?
- @path = "#{@path}.:format"
- elsif optional_format?
- @path = "#{@path}(.:format)"
+ if format == true
+ "#{path}.:format"
+ elsif optional_format?(path, format)
+ "#{path}(.:format)"
+ else
+ path
end
end
- def required_format?
- options[:format] == true
+ def optional_format?(path, format)
+ format != false && !path.include?(':format') && !path.end_with?('/')
end
- def optional_format?
- options[:format] != false && !path.include?(':format') && !path.end_with?('/')
- end
-
- def normalize_options!
- @options.reverse_merge!(scope[:options]) if scope[:options]
- path_without_format = path.sub(/\(\.:format\)$/, '')
-
+ def normalize_options!(options, formatted, path_params, path_ast, modyoule)
# Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default
- if path_without_format.match(WILDCARD_PATH) && @options[:format] != false
- @options[$1.to_sym] ||= /.+?/
+ if formatted != false
+ path_ast.grep(Journey::Nodes::Star) do |node|
+ options[node.name.to_sym] ||= /.+?/
+ end
end
- if path_without_format.match(':controller')
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]
+ if path_params.include?(:controller)
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
# Add a default constraint for :controller path segments that matches namespaced
# controllers with default routes like :controller/:action/:id(.:format), e.g:
# GET /admin/products/show/1
# => { controller: 'admin/products', action: 'show', id: '1' }
- @options[:controller] ||= /.+?/
+ options[:controller] ||= /.+?/
end
- @options.merge!(default_controller_and_action)
+ if to.respond_to? :call
+ options
+ else
+ to_endpoint = split_to to
+ controller = to_endpoint[0] || default_controller
+ action = to_endpoint[1] || default_action
+
+ controller = add_controller_module(controller, modyoule)
+
+ options.merge! check_controller_and_action(path_params, controller, action)
+ end
end
- def normalize_requirements!
- constraints.each do |key, requirement|
- next unless segment_keys.include?(key) || key == :controller
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
- @requirements[key] = requirement
+ def split_constraints(path_params, constraints)
+ constraints.each_pair do |key, requirement|
+ if path_params.include?(key) || key == :controller
+ verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
+ @requirements[key] = requirement
+ else
+ @conditions[key] = requirement
+ end
end
+ end
- if options[:format] == true
+ def normalize_format!(formatted)
+ if formatted == true
@requirements[:format] ||= /.+/
- elsif Regexp === options[:format]
- @requirements[:format] = options[:format]
- elsif String === options[:format]
- @requirements[:format] = Regexp.compile(options[:format])
+ elsif Regexp === formatted
+ @requirements[:format] = formatted
+ @defaults[:format] = nil
+ elsif String === formatted
+ @requirements[:format] = Regexp.compile(formatted)
+ @defaults[:format] = formatted
end
end
@@ -143,169 +211,143 @@ module ActionDispatch
end
end
- def normalize_defaults!
- @defaults.merge!(scope[:defaults]) if scope[:defaults]
- @defaults.merge!(options[:defaults]) if options[:defaults]
-
- options.each do |key, default|
- unless Regexp === default || IGNORE_OPTIONS.include?(key)
+ def normalize_defaults!(options)
+ options.each_pair do |key, default|
+ unless Regexp === default
@defaults[key] = default
end
end
-
- if options[:constraints].is_a?(Hash)
- options[:constraints].each do |key, default|
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
- @defaults[key] ||= default
- end
- end
- end
-
- if Regexp === options[:format]
- @defaults[:format] = nil
- elsif String === options[:format]
- @defaults[:format] = options[:format]
- end
end
- def normalize_conditions!
- @conditions[:path_info] = path
-
- constraints.each do |key, condition|
- unless segment_keys.include?(key) || key == :controller
- @conditions[key] = condition
- end
- end
-
- required_defaults = []
- options.each do |key, required_default|
- unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
- required_defaults << key
- end
+ def verify_callable_constraint(callable_constraint)
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
end
- @conditions[:required_defaults] = required_defaults
+ end
- via_all = options.delete(:via) if options[:via] == :all
+ def add_request_method(via, conditions)
+ return if via == [:all]
- if !via_all && options[:via].blank?
+ if via.empty?
msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
"If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
"If you want to expose your action to GET, use `get` in the router:\n" \
" Instead of: match \"controller#action\"\n" \
" Do: get \"controller#action\""
- raise msg
+ raise ArgumentError, msg
end
- if via = options[:via]
- @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase }
- end
+ conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
end
- def app
- Constraints.new(endpoint, blocks, @set.request_class)
- end
+ def app(blocks)
+ return to if Redirect === to
- def default_controller_and_action
if to.respond_to?(:call)
- { }
+ Constraints.new(to, blocks, false)
else
- if to.is_a?(String)
- controller, action = to.split('#')
- elsif to.is_a?(Symbol)
- action = to.to_s
- end
-
- controller ||= default_controller
- action ||= default_action
-
- 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/}
- raise ArgumentError, "controller name should not start with a slash"
+ if blocks.any?
+ Constraints.new(dispatcher(defaults), blocks, true)
+ else
+ dispatcher(defaults)
end
+ end
+ end
- controller = controller.to_s unless controller.is_a?(Regexp)
- action = action.to_s unless action.is_a?(Regexp)
+ def check_controller_and_action(path_params, controller, action)
+ hash = check_part(:controller, controller, path_params, {}) do |part|
+ translate_controller(part) {
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
- if controller.blank? && segment_keys.exclude?(:controller)
- message = "Missing :controller key on routes definition, please check your routes."
raise ArgumentError, message
- end
+ }
+ end
- if action.blank? && segment_keys.exclude?(:action)
- message = "Missing :action key on routes definition, please check your routes."
- raise ArgumentError, message
- end
+ check_part(:action, action, path_params, hash) { |part|
+ part.is_a?(Regexp) ? part : part.to_s
+ }
+ end
- if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/
- message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems."
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
+ def check_part(name, part, path_params, hash)
+ if part
+ hash[name] = yield(part)
+ else
+ unless path_params.include?(name)
+ message = "Missing :#{name} key on routes definition, please check your routes."
raise ArgumentError, message
end
-
- hash = {}
- hash[:controller] = controller unless controller.blank?
- hash[:action] = action unless action.blank?
- hash
end
- end
-
- def blocks
- if options[:constraints].present? && !options[:constraints].is_a?(Hash)
- [options[:constraints]]
+ hash
+ end
+
+ def split_to(to)
+ case to
+ when Symbol
+ ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\""
+ [nil, to.to_s]
+ when /#/ then to.split('#')
+ when String
+ ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\""
+ [to, nil]
else
- scope[:blocks] || []
+ []
end
end
- def constraints
- @constraints ||= {}.tap do |constraints|
- constraints.merge!(scope[:constraints]) if scope[:constraints]
-
- options.except(*IGNORE_OPTIONS).each do |key, option|
- constraints[key] = option if Regexp === option
+ def add_controller_module(controller, modyoule)
+ if modyoule && !controller.is_a?(Regexp)
+ if controller =~ %r{\A/}
+ controller[1..-1]
+ else
+ [modyoule, controller].compact.join("/")
end
-
- constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
+ else
+ controller
end
end
- def segment_keys
- @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
- end
-
- def path_pattern
- Journey::Path::Pattern.new(strexp)
- end
+ def translate_controller(controller)
+ return controller if Regexp === controller
+ return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
- def strexp
- Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
+ yield
end
- def endpoint
- to.respond_to?(:call) ? to : dispatcher
+ def blocks(options_constraints, scope_blocks)
+ if options_constraints && !options_constraints.is_a?(Hash)
+ verify_callable_constraint(options_constraints)
+ [options_constraints]
+ else
+ scope_blocks || []
+ end
end
- def dispatcher
- Routing::RouteSet::Dispatcher.new(:defaults => defaults)
+ def constraints(options, path_params)
+ constraints = {}
+ required_defaults = []
+ options.each_pair do |key, option|
+ if Regexp === option
+ constraints[key] = option
+ else
+ required_defaults << key unless path_params.include?(key)
+ end
+ end
+ @conditions[:required_defaults] = required_defaults
+ constraints
end
- def to
- options[:to]
+ def path_params(ast)
+ ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
end
- def default_controller
- options[:controller] || scope[:controller]
+ def path_ast(path)
+ parser = Journey::Parser.new
+ parser.parse path
end
- def default_action
- options[:action] || scope[:action]
+ def dispatcher(defaults)
+ @set.dispatcher defaults
end
end
@@ -340,37 +382,57 @@ 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 requesting various HTTP verbs with a single action has security
# implications, you must either specify the actions in
@@ -387,13 +449,19 @@ module ActionDispatch
# [:action]
# The route's action.
#
+ # [:param]
+ # Overrides the default resource identifier `:id` (name of the
+ # dynamic segment used to generate the routes).
+ # You can access that segment from your controller using
+ # <tt>params[<:param>]</tt>.
+ #
# [:path]
# The path prefix for the routes.
#
# [:module]
# The namespace for :controller.
#
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
# # => Sekret::PostsController
#
# See <tt>Scoping#namespace</tt> for its scope equivalent.
@@ -412,9 +480,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
@@ -439,14 +507,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 Whitelist
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path', to: 'c#a', constraints: Whitelist.new
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -455,7 +523,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.
#
@@ -464,7 +532,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+
@@ -506,13 +574,21 @@ module ActionDispatch
raise "A rack application must be specified" unless path
- options[:as] ||= app_name(app)
+ rails_app = rails_app? app
+
+ if rails_app
+ options[:as] ||= app.railtie_name
+ else
+ # non rails apps can't have an :as
+ options[:as] = nil
+ end
+
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, target_as)
+ define_generate_prefix(app, target_as) if rails_app
self
end
@@ -533,35 +609,27 @@ module ActionDispatch
end
private
- def app_name(app)
- return unless app.respond_to?(:routes)
-
- if app.respond_to?(:railtie_name)
- app.railtie_name
- else
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
- ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
- end
+ def rails_app?(app)
+ app.is_a?(Class) && app < Rails::Railtie
end
def define_generate_prefix(app, name)
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
-
- _route = @set.named_routes.routes[name.to_sym]
+ _route = @set.named_routes.get name
_routes = @set
app.routes.define_mounted_helper(name)
- app.routes.singleton_class.class_eval do
- redefine_method :mounted? do
- true
- end
-
- 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) }
- _routes.url_helpers.send("#{name}_path", prefix_options)
+ app.routes.extend Module.new {
+ def optimize_routes_generation?; false; end
+ define_method :find_script_name do |options|
+ if options.key? :script_name
+ super(options)
+ else
+ 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) }
+ _routes.url_helpers.send("#{name}_path", prefix_options)
+ end
end
- end
+ }
end
end
@@ -648,7 +716,7 @@ module ActionDispatch
# resources :posts, module: "admin"
#
# If you want to route /admin/posts to +PostsController+
- # (without the Admin:: module prefix), you could use
+ # (without the <tt>Admin::</tt> module prefix), you could use
#
# scope "/admin" do
# resources :posts
@@ -702,13 +770,14 @@ module ActionDispatch
# end
def scope(*args)
options = args.extract_options!.dup
- recover = {}
+ scope = {}
options[:path] = args.flatten.join('/') if args.any?
options[:constraints] ||= {}
- unless shallow?
- options[:shallow_path] = options[:path] if args.any?
+ 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)
@@ -721,7 +790,7 @@ module ActionDispatch
block, options[:constraints] = options[:constraints], {}
end
- SCOPE_OPTIONS.each do |option|
+ @scope.options.each do |option|
if option == :blocks
value = block
elsif option == :options
@@ -731,15 +800,15 @@ module ActionDispatch
end
if value
- recover[option] = @scope[option]
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
+ @scope = @scope.new scope
yield
self
ensure
- @scope.merge!(recover)
+ @scope = @scope.parent
end
# Scopes routes to a specific controller
@@ -792,9 +861,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
@@ -970,8 +1046,6 @@ module ActionDispatch
VALID_ON_OPTIONS = [:new, :collection, :member]
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
- RESOURCE_SCOPES = [:resource, :resources]
class Resource #:nodoc:
attr_reader :controller, :path, :options, :param
@@ -983,6 +1057,7 @@ module ActionDispatch
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
+ @shallow = false
end
def default_actions
@@ -1043,6 +1118,13 @@ module ActionDispatch
"#{path}/:#{nested_param}"
end
+ def shallow=(value)
+ @shallow = value
+ end
+
+ def shallow?
+ @shallow
+ end
end
class SingletonResource < Resource #:nodoc:
@@ -1323,8 +1405,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
@@ -1347,16 +1431,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
@@ -1389,7 +1465,20 @@ module ActionDispatch
if rest.empty? && Hash === path
options = path
path, to = options.find { |name, _value| name.is_a?(String) }
- options[:to] = to
+
+ case to
+ when Symbol
+ options[:action] = to
+ when String
+ if to =~ /#/
+ options[:to] = to
+ else
+ options[:controller] = to
+ end
+ else
+ options[:to] = to
+ end
+
options.delete(path)
paths = [path]
else
@@ -1430,7 +1519,7 @@ module ActionDispatch
if on = options.delete(:on)
send(on) { decomposed_match(path, options) }
else
- case @scope[:scope_level]
+ case @scope.scope_level
when :resources
nested { decomposed_match(path, options) }
when :resource
@@ -1443,6 +1532,8 @@ module ActionDispatch
def add_route(action, options) # :nodoc:
path = path_for_action(action, options.delete(:path))
+ raise ArgumentError, "path is required" if path.blank?
+
action = action.to_s.dup
if action =~ /^[\w\-\/]+$/
@@ -1451,13 +1542,13 @@ module ActionDispatch
action = nil
end
- if !options.fetch(:as, true)
- options.delete(:as)
- else
- options[:as] = name_for_action(options[:as], action)
- end
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
- mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
@@ -1471,7 +1562,7 @@ module ActionDispatch
raise ArgumentError, "must be called with a path and/or options"
end
- if @scope[:scope_level] == :resources
+ if @scope.resources?
with_scope_level(:root) do
scope(parent_resource.path) do
super(options)
@@ -1538,41 +1629,47 @@ module ActionDispatch
end
def resource_scope? #:nodoc:
- RESOURCE_SCOPES.include? @scope[:scope_level]
+ @scope.resource_scope?
end
def resource_method_scope? #:nodoc:
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
+ @scope.resource_method_scope?
+ end
+
+ def nested_scope? #:nodoc:
+ @scope.nested?
end
def with_exclusive_scope
begin
- old_name_prefix, old_path = @scope[:as], @scope[:path]
- @scope[:as], @scope[:path] = nil, nil
+ @scope = @scope.new(:as => nil, :path => nil)
with_scope_level(:exclusive) do
yield
end
ensure
- @scope[:as], @scope[:path] = old_name_prefix, old_path
+ @scope = @scope.parent
end
end
- def with_scope_level(kind, resource = parent_resource)
- old, @scope[:scope_level] = @scope[:scope_level], kind
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
+ def with_scope_level(kind)
+ @scope = @scope.new_level(kind)
yield
ensure
- @scope[:scope_level] = old
- @scope[:scope_level_resource] = old_resource
+ @scope = @scope.parent
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]
+ @scope = @scope.new(:scope_level_resource => resource)
+ @nesting.push(resource)
+
+ with_scope_level(kind) do
+ scope(parent_resource.resource_scope) { yield }
end
+ ensure
+ @nesting.pop
+ @scope = @scope.parent
end
def nested_options #:nodoc:
@@ -1584,6 +1681,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
@@ -1592,22 +1697,25 @@ module ActionDispatch
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action, flag) #:nodoc:
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ def canonical_action?(action) #:nodoc:
+ 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:
+ scope = { :as => @scope[:shallow_prefix],
+ :path => @scope[:shallow_path] }
+ @scope = @scope.new scope
+
+ scope(path, options) { yield }
+ ensure
+ @scope = @scope.parent
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
+ if path.blank? && canonical_action?(action)
+ @scope[:path].to_s
else
- "#{prefix}/#{action_path(action, path)}"
+ "#{@scope[:path]}/#{action_path(action, path)}"
end
end
@@ -1619,15 +1727,17 @@ module ActionDispatch
def prefix_name_for_action(as, action) #:nodoc:
if as
prefix = as
- elsif !canonical_action?(action, @scope[:scope_level])
+ elsif !canonical_action?(action)
prefix = action
end
- prefix.to_s.tr('-', '_') if prefix
+
+ if prefix && prefix != '/' && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
+ end
end
def name_for_action(as, action) #:nodoc:
prefix = prefix_name_for_action(as, action)
- prefix = Mapper.normalize_name(prefix) if prefix
name_prefix = @scope[:as]
if parent_resource
@@ -1637,27 +1747,14 @@ module ActionDispatch
member_name = parent_resource.member_name
end
- name = case @scope[:scope_level]
- when :nested
- [name_prefix, prefix]
- when :collection
- [prefix, name_prefix, collection_name]
- when :new
- [prefix, :new, name_prefix, member_name]
- when :member
- [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
- when :root
- [name_prefix, collection_name, prefix]
- else
- [name_prefix, member_name, prefix]
- end
+ name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- if candidate = name.select(&:present?).join("_").presence
+ if candidate = name.compact.join("_").presence
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
+ candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
else
candidate
end
@@ -1782,10 +1879,85 @@ module ActionDispatch
end
end
+ class Scope # :nodoc:
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
+ :controller, :action, :path_names, :constraints,
+ :shallow, :blocks, :defaults, :options]
+
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
+
+ attr_reader :parent, :scope_level
+
+ def initialize(hash, parent = {}, scope_level = nil)
+ @hash = hash
+ @parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case scope_level
+ when :nested
+ [name_prefix, prefix]
+ when :collection
+ [prefix, name_prefix, collection_name]
+ when :new
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
+ else
+ [name_prefix, member_name, prefix]
+ end
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
+ end
+
+ def options
+ OPTIONS
+ end
+
+ def new(hash)
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(self, self, level)
+ end
+
+ def fetch(key, &block)
+ @hash.fetch(key, &block)
+ end
+
+ def [](key)
+ @hash.fetch(key) { @parent[key] }
+ end
+
+ def []=(k,v)
+ @hash[k] = v
+ end
+ end
+
def initialize(set) #:nodoc:
@set = set
- @scope = { :path_names => @set.resources_path_names }
+ @scope = Scope.new({ :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 2fb03f2712..bd3696cda1 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -101,53 +101,45 @@ module ActionDispatch
# polymorphic_url(Comment) # same as comments_url()
#
def polymorphic_url(record_or_hash_or_array, options = {})
- 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.first.is_a?(ActionDispatch::Routing::RoutesProxy)
- proxy = 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
-
- record = extract_record(record_or_hash_or_array)
- record = convert_to_model(record)
-
- args = Array === record_or_hash_or_array ?
- record_or_hash_or_array.dup :
- [ record_or_hash_or_array ]
-
- inflection = if options[:action] && options[:action].to_s == "new"
- args.pop
- :singular
- elsif (record.respond_to?(:persisted?) && !record.persisted?)
- args.pop
- :plural
- elsif record.is_a?(Class)
- args.pop
- :plural
- else
- :singular
+ if Hash === record_or_hash_or_array
+ options = record_or_hash_or_array.merge(options)
+ record = options.delete :id
+ return polymorphic_url record, options
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)
-
- url_options = options.except(:action, :routing_type)
- unless url_options.empty?
- args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
- end
+ opts = options.dup
+ action = opts.delete :action
+ type = opts.delete(:routing_type) || :url
- args.collect! { |a| convert_to_model(a) }
+ HelperMethodBuilder.polymorphic_method self,
+ record_or_hash_or_array,
+ action,
+ type,
+ opts
- (proxy || self).send(named_route, *args)
end
# Returns the path component of a URL for the given record. It uses
# <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
def polymorphic_path(record_or_hash_or_array, options = {})
- polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path))
+ if Hash === record_or_hash_or_array
+ options = record_or_hash_or_array.merge(options)
+ record = options.delete :id
+ return polymorphic_path record, options
+ end
+
+ opts = options.dup
+ action = opts.delete :action
+ type = :path
+
+ HelperMethodBuilder.polymorphic_method self,
+ record_or_hash_or_array,
+ action,
+ type,
+ opts
end
+
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
@@ -165,54 +157,169 @@ module ActionDispatch
end
private
- def action_prefix(options)
- options[:action] ? "#{options[:action]}_" : ''
+
+ class HelperMethodBuilder # :nodoc:
+ CACHE = { 'path' => {}, 'url' => {} }
+
+ def self.get(action, type)
+ type = type.to_s
+ CACHE[type].fetch(action) { build action, type }
+ end
+
+ def self.url; CACHE['url'.freeze][nil]; end
+ def self.path; CACHE['path'.freeze][nil]; end
+
+ def self.build(action, type)
+ prefix = action ? "#{action}_" : ""
+ suffix = type
+ if action.to_s == 'new'
+ HelperMethodBuilder.singular prefix, suffix
+ else
+ HelperMethodBuilder.plural prefix, suffix
+ end
+ end
+
+ def self.singular(prefix, suffix)
+ new(->(name) { name.singular_route_key }, prefix, suffix)
end
- def routing_type(options)
- options[:routing_type] || :url
+ def self.plural(prefix, suffix)
+ new(->(name) { name.route_key }, prefix, suffix)
end
- def build_named_route_call(records, inflection, options = {})
- if records.is_a?(Array)
- record = records.pop
- route = records.map do |parent|
- if parent.is_a?(Symbol) || parent.is_a?(String)
- parent
- else
- model_name_from_record_or_class(parent).singular_route_key
- end
+ def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
+ builder = get action, type
+
+ case record_or_hash_or_array
+ when Array
+ if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil)
+ raise ArgumentError, "Nil location provided. Can't build URI."
end
+ if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
+ recipient = record_or_hash_or_array.shift
+ end
+
+ method, args = builder.handle_list record_or_hash_or_array
+ when String, Symbol
+ method, args = builder.handle_string record_or_hash_or_array
+ when Class
+ method, args = builder.handle_class record_or_hash_or_array
+
+ when nil
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ else
+ method, args = builder.handle_model record_or_hash_or_array
+ end
+
+
+ if options.empty?
+ recipient.send(method, *args)
else
- record = extract_record(records)
- route = []
+ recipient.send(method, *args, options)
end
+ end
+
+ attr_reader :suffix, :prefix
- if record.is_a?(Symbol) || record.is_a?(String)
- route << record
- elsif record
- if inflection == :singular
- route << model_name_from_record_or_class(record).singular_route_key
+ def initialize(key_strategy, prefix, suffix)
+ @key_strategy = key_strategy
+ @prefix = prefix
+ @suffix = suffix
+ end
+
+ def handle_string(record)
+ [get_method_for_string(record), []]
+ end
+
+ def handle_string_call(target, str)
+ target.send get_method_for_string str
+ end
+
+ def handle_class(klass)
+ [get_method_for_class(klass), []]
+ end
+
+ def handle_class_call(target, klass)
+ target.send get_method_for_class klass
+ end
+
+ def handle_model(record)
+ args = []
+
+ model = record.to_model
+ name = if record.persisted?
+ args << model
+ model.class.model_name.singular_route_key
+ else
+ @key_strategy.call model.class.model_name
+ end
+
+ named_route = prefix + "#{name}_#{suffix}"
+
+ [named_route, args]
+ end
+
+ def handle_model_call(target, model)
+ method, args = handle_model model
+ target.send(method, *args)
+ end
+
+ def handle_list(list)
+ record_list = list.dup
+ record = record_list.pop
+
+ args = []
+
+ route = record_list.map { |parent|
+ case parent
+ when Symbol, String
+ parent.to_s
+ when Class
+ args << parent
+ parent.model_name.singular_route_key
else
- route << model_name_from_record_or_class(record).route_key
+ args << parent.to_model
+ parent.to_model.class.model_name.singular_route_key
end
+ }
+
+ route <<
+ case record
+ when Symbol, String
+ record.to_s
+ when Class
+ @key_strategy.call record.model_name
else
- raise ArgumentError, "Nil location provided. Can't build URI."
+ if record.persisted?
+ args << record.to_model
+ record.to_model.class.model_name.singular_route_key
+ else
+ @key_strategy.call record.to_model.class.model_name
+ end
end
- route << routing_type(options)
+ route << suffix
- action_prefix(options) + route.join("_")
+ named_route = prefix + route.join("_")
+ [named_route, args]
end
- def extract_record(record_or_hash_or_array)
- case record_or_hash_or_array
- when Array; record_or_hash_or_array.last
- when Hash; record_or_hash_or_array[:id]
- else record_or_hash_or_array
- end
+ private
+
+ def get_method_for_class(klass)
+ name = @key_strategy.call klass.model_name
+ prefix + "#{name}_#{suffix}"
+ end
+
+ def get_method_for_string(str)
+ prefix + "#{str}_#{suffix}"
end
+
+ [nil, 'new', 'edit'].each do |action|
+ CACHE['url'][action] = build action, 'url'
+ CACHE['path'][action] = build action, 'path'
+ end
+ end
end
end
end
-
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index b08e62543b..3c1c4fadf6 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -3,10 +3,11 @@ require 'active_support/core_ext/uri'
require 'active_support/core_ext/array/extract_options'
require 'rack/utils'
require 'action_controller/metal/exceptions'
+require 'action_dispatch/routing/endpoint'
module ActionDispatch
module Routing
- class Redirect # :nodoc:
+ class Redirect < Endpoint # :nodoc:
attr_reader :status, :block
def initialize(status, block)
@@ -14,18 +15,15 @@ module ActionDispatch
@block = block
end
- def call(env)
- req = Request.new(env)
+ def redirect?; true; end
- # 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?
- raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
- end
- end
+ def call(env)
+ serve Request.new env
+ end
- uri = URI.parse(path(req.symbolized_path_parameters, req))
+ def serve(req)
+ req.check_path_parameters!
+ uri = URI.parse(path(req.path_parameters, req))
unless uri.host
if relative_path?(uri.path)
@@ -39,7 +37,7 @@ module ActionDispatch
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
- body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
+ body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
'Location' => uri.to_s,
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index a03fb4cee7..5b3651aaee 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,11 +1,14 @@
require 'action_dispatch/journey'
require 'forwardable'
require 'thread_safe'
+require 'active_support/concern'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/array/extract_options'
require 'action_controller/metal/exceptions'
+require 'action_dispatch/http/request'
+require 'action_dispatch/routing/endpoint'
module ActionDispatch
module Routing
@@ -16,27 +19,17 @@ module ActionDispatch
# alias inspect to to_s.
alias inspect to_s
- PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
-
- class Dispatcher #:nodoc:
- def initialize(options={})
- @defaults = options[:defaults]
- @glob_param = options.delete(:glob)
+ class Dispatcher < Routing::Endpoint #:nodoc:
+ def initialize(defaults)
+ @defaults = defaults
@controller_class_names = ThreadSafe::Cache.new
end
- def call(env)
- params = env[PARAMETERS_KEY]
-
- # 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?)
+ def dispatcher?; true; end
- unless value.valid_encoding?
- raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
- end
- end
+ def serve(req)
+ req.check_path_parameters!
+ params = req.path_parameters
prepare_params!(params)
@@ -45,13 +38,12 @@ module ActionDispatch
return [404, {'X-Cascade' => 'pass'}, []]
end
- dispatch(controller, params[:action], env)
+ dispatch(controller, params[:action], req.env)
end
def prepare_params!(params)
normalize_controller!(params)
merge_default_action!(params)
- split_glob_param!(params) if @glob_param
end
# If this is a default_controller (i.e. a controller specified by the user)
@@ -87,10 +79,6 @@ module ActionDispatch
def merge_default_action!(params)
params[:action] ||= 'index'
end
-
- def split_glob_param!(params)
- params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
- end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -98,36 +86,64 @@ module ActionDispatch
# named routes.
class NamedRouteCollection #:nodoc:
include Enumerable
- attr_reader :routes, :helpers, :module
+ attr_reader :routes, :url_helpers_module
def initialize
@routes = {}
- @helpers = []
- @module = Module.new
+ @path_helpers = Set.new
+ @url_helpers = Set.new
+ @url_helpers_module = Module.new
+ @path_helpers_module = Module.new
+ end
+
+ def route_defined?(name)
+ key = name.to_sym
+ @path_helpers.include?(key) || @url_helpers.include?(key)
end
def helper_names
- @helpers.map(&:to_s)
+ @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
end
def clear!
- @helpers.each do |helper|
- @module.remove_possible_method helper
+ @path_helpers.each do |helper|
+ @path_helpers_module.send :undef_method, helper
+ end
+
+ @url_helpers.each do |helper|
+ @url_helpers_module.send :undef_method, helper
end
@routes.clear
- @helpers.clear
+ @path_helpers.clear
+ @url_helpers.clear
end
def add(name, route)
- routes[name.to_sym] = route
- define_named_route_methods(name, route)
+ key = name.to_sym
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ if routes.key? key
+ @path_helpers_module.send :undef_method, path_name
+ @url_helpers_module.send :undef_method, url_name
+ end
+ routes[key] = route
+ define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
+ define_url_helper @url_helpers_module, route, url_name, route.defaults, name, FULL
+
+ @path_helpers << path_name
+ @url_helpers << url_name
end
def get(name)
routes[name.to_sym]
end
+ def key?(name)
+ routes.key? name.to_sym
+ end
+
alias []= add
alias [] get
alias clear clear!
@@ -145,36 +161,54 @@ module ActionDispatch
routes.length
end
+ def path_helpers_module(warn = false)
+ if warn
+ mod = @path_helpers_module
+ helpers = @path_helpers
+ Module.new do
+ include mod
+
+ helpers.each do |meth|
+ define_method(meth) do |*args, &block|
+ ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead")
+ super(*args, &block)
+ end
+ end
+ end
+ else
+ @path_helpers_module
+ end
+ end
+
class UrlHelper # :nodoc:
- def self.create(route, options)
+ def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
- OptimizedUrlHelper.new(route, options)
+ OptimizedUrlHelper.new(route, options, route_name, url_strategy)
else
- new route, options
+ new route, options, route_name, url_strategy
end
end
def self.optimize_helper?(route)
- route.requirements.except(:controller, :action).empty?
+ !route.glob? && route.path.requirements.empty?
end
+ attr_reader :url_strategy, :route_name
+
class OptimizedUrlHelper < UrlHelper # :nodoc:
attr_reader :arg_size
- def initialize(route, options)
+ def initialize(route, options, route_name, url_strategy)
super
- @klass = Journey::Router::Utils
@required_parts = @route.required_parts
@arg_size = @required_parts.size
- @optimized_path = @route.optimized_path
end
- def call(t, args)
- if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t)
- options = @options.dup
- options.merge!(t.url_options) if t.respond_to?(:url_options)
+ def call(t, args, inner_options)
+ if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
+ options = t.url_options.merge @options
options[:path] = optimized_helper(args)
- ActionDispatch::Http::URL.url_for(options)
+ url_strategy.call options
else
super
end
@@ -183,18 +217,14 @@ module ActionDispatch
private
def optimized_helper(args)
- params = Hash[parameterize_args(args)]
+ params = parameterize_args(args)
missing_keys = missing_keys(params)
unless missing_keys.empty?
raise_generation_error(params, missing_keys)
end
- @optimized_path.map{ |segment| replace_segment(params, segment) }.join
- end
-
- def replace_segment(params, segment)
- Symbol === segment ? @klass.escape_fragment(params[segment]) : segment
+ @route.format params
end
def optimize_routes_generation?(t)
@@ -202,7 +232,9 @@ module ActionDispatch
end
def parameterize_args(args)
- @required_parts.zip(args.map(&:to_param))
+ params = {}
+ @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v }
+ params
end
def missing_keys(args)
@@ -218,27 +250,36 @@ module ActionDispatch
end
end
- def initialize(route, options)
+ def initialize(route, options, route_name, url_strategy)
@options = options
@segment_keys = route.segment_keys.uniq
@route = route
+ @url_strategy = url_strategy
+ @route_name = route_name
end
- def call(t, args)
- t.url_for(handle_positional_args(t, args, @options, @segment_keys))
+ def call(t, args, inner_options)
+ controller_options = t.url_options
+ options = controller_options.merge @options
+ hash = handle_positional_args(controller_options,
+ inner_options || {},
+ args,
+ options,
+ @segment_keys)
+
+ t._routes.url_for(hash, route_name, url_strategy)
end
- def handle_positional_args(t, args, options, keys)
- inner_options = args.extract_options!
- result = options.dup
+ def handle_positional_args(controller_options, inner_options, args, result, path_params)
if args.size > 0
- if args.size < keys.size - 1 # take format into account
- keys -= t.url_options.keys if t.respond_to?(:url_options)
- keys -= options.keys
+ if args.size < path_params.size - 1 # take format into account
+ path_params -= controller_options.keys
+ path_params -= result.keys
end
- keys -= inner_options.keys
- result.merge!(Hash[keys.zip(args)])
+ path_params.each { |param|
+ result[param] = inner_options[param] || args.shift
+ }
end
result.merge!(inner_options)
@@ -259,27 +300,25 @@ module ActionDispatch
#
# foo_url(bar, baz, bang, sort_by: 'baz')
#
- def define_url_helper(route, name, options)
- helper = UrlHelper.create(route, options.dup)
-
- @module.remove_possible_method name
- @module.module_eval do
+ def define_url_helper(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
define_method(name) do |*args|
- helper.call self, args
+ options = nil
+ options = args.pop if args.last.is_a? Hash
+ helper.call self, args, options
end
end
-
- helpers << name
- end
-
- def define_named_route_methods(name, route)
- define_url_helper route, :"#{name}_path",
- route.defaults.merge(:use_route => name, :only_path => true)
- define_url_helper route, :"#{name}_url",
- route.defaults.merge(:use_route => name, :only_path => false)
end
end
+ # :stopdoc:
+ # strategy for building urls to send to the client
+ PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
+ FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) }
+ UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
+ # :startdoc:
+
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
attr_accessor :default_url_options, :request_class
@@ -292,7 +331,7 @@ module ActionDispatch
def initialize(request_class = ActionDispatch::Request)
self.named_routes = NamedRouteCollection.new
- self.resources_path_names = self.class.default_resources_path_names.dup
+ self.resources_path_names = self.class.default_resources_path_names
self.default_url_options = {}
self.request_class = request_class
@@ -302,9 +341,7 @@ module ActionDispatch
@finalized = false
@set = Journey::Routes.new
- @router = Journey::Router.new(@set, {
- :parameters_key => PARAMETERS_KEY,
- :request_class => request_class})
+ @router = Journey::Router.new @set
@formatter = Journey::Formatter.new @set
end
@@ -335,6 +372,7 @@ module ActionDispatch
mapper.instance_exec(&block)
end
end
+ private :eval_block
def finalize!
return if @finalized
@@ -350,6 +388,10 @@ module ActionDispatch
@prepend.each { |blk| eval_block(blk) }
end
+ def dispatcher(defaults)
+ Routing::RouteSet::Dispatcher.new(defaults)
+ end
+
module MountedHelpers #:nodoc:
extend ActiveSupport::Concern
include UrlFor
@@ -380,40 +422,51 @@ module ActionDispatch
RUBY
end
- def url_helpers
- @url_helpers ||= begin
- routes = self
+ def url_helpers(include_path_helpers = true)
+ routes = self
- Module.new do
- extend ActiveSupport::Concern
- include UrlFor
+ Module.new do
+ extend ActiveSupport::Concern
+ include UrlFor
+
+ # Define url_for in the singleton level so one can do:
+ # Rails.application.routes.url_helpers.url_for(args)
+ @_routes = routes
+ class << self
+ delegate :url_for, :optimize_routes_generation?, to: '@_routes'
+ attr_reader :_routes
+ def url_options; {}; end
+ end
- # Define url_for in the singleton level so one can do:
- # Rails.application.routes.url_helpers.url_for(args)
- @_routes = routes
- class << self
- delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
- end
+ url_helpers = routes.named_routes.url_helpers_module
- # Make named_routes available in the module singleton
- # as well, so one can do:
- # Rails.application.routes.url_helpers.posts_path
- extend routes.named_routes.module
+ # Make named_routes available in the module singleton
+ # as well, so one can do:
+ # Rails.application.routes.url_helpers.posts_path
+ extend url_helpers
- # Any class that includes this module will get all
- # named routes...
- include routes.named_routes.module
+ # Any class that includes this module will get all
+ # named routes...
+ include url_helpers
- # plus a singleton class method called _routes ...
- included do
- singleton_class.send(:redefine_method, :_routes) { routes }
- end
+ if include_path_helpers
+ path_helpers = routes.named_routes.path_helpers_module
+ else
+ path_helpers = routes.named_routes.path_helpers_module(true)
+ end
- # And an instance method _routes. Note that
- # UrlFor (included in this module) add extra
- # conveniences for working with @_routes.
- define_method(:_routes) { @_routes || routes }
+ include path_helpers
+ extend path_helpers
+
+ # plus a singleton class method called _routes ...
+ included do
+ singleton_class.send(:redefine_method, :_routes) { routes }
end
+
+ # And an instance method _routes. Note that
+ # UrlFor (included in this module) add extra
+ # conveniences for working with @_routes.
+ define_method(:_routes) { @_routes || routes }
end
end
@@ -432,7 +485,9 @@ module ActionDispatch
"http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end
- path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
+ path = conditions.delete :path_info
+ ast = conditions.delete :parsed_path_info
+ path = build_path(path, ast, requirements, anchor)
conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
route = @set.add_route(app, path, conditions, defaults, name)
@@ -440,8 +495,9 @@ module ActionDispatch
route
end
- def build_path(path, requirements, separators, anchor)
+ def build_path(path, ast, requirements, anchor)
strexp = Journey::Router::Strexp.new(
+ ast,
path,
requirements,
SEPARATORS,
@@ -502,8 +558,8 @@ module ActionDispatch
attr_reader :options, :recall, :set, :named_route
- def initialize(options, recall, set)
- @named_route = options.delete(:use_route)
+ def initialize(named_route, options, recall, set)
+ @named_route = named_route
@options = options.dup
@recall = recall.dup
@set = set
@@ -594,7 +650,7 @@ module ActionDispatch
# Generates a path from routes, returns [path, params].
# If no route is generated the formatter will raise ActionController::UrlGenerationError
def generate
- @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
+ @set.formatter.generate(named_route, options, recall, PARAMETERIZE)
end
def different_controller?
@@ -619,61 +675,74 @@ module ActionDispatch
end
def generate_extras(options, recall={})
- path, params = generate(options, recall)
+ route_key = options.delete :use_route
+ path, params = generate(route_key, options, recall)
return path, params.keys
end
- def generate(options, recall = {})
- Generator.new(options, recall, self).generate
+ def generate(route_key, options, recall = {})
+ Generator.new(route_key, options, recall, self).generate
end
+ private :generate
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
:original_script_name]
- def mounted?
- false
+ def optimize_routes_generation?
+ default_url_options.empty?
end
- def optimize_routes_generation?
- !mounted? && default_url_options.empty?
+ def find_script_name(options)
+ options.delete(:script_name) { '' }
end
- def _generate_prefix(options = {})
- nil
+ def path_for(options, route_name = nil) # :nodoc:
+ url_for(options, route_name, PATH)
end
- # The +options+ argument must be +nil+ or a hash whose keys are *symbols*.
- def url_for(options)
- options = default_url_options.merge(options || {})
+ # The +options+ argument must be a hash whose keys are *symbols*.
+ def url_for(options, route_name = nil, url_strategy = UNKNOWN)
+ options = default_url_options.merge options
- user, password = extract_authentication(options)
- recall = options.delete(:_recall)
+ user = password = nil
- original_script_name = options.delete(:original_script_name).presence
- script_name = options.delete(:script_name).presence || _generate_prefix(options)
+ if options[:user] && options[:password]
+ user = options.delete :user
+ password = options.delete :password
+ end
+
+ recall = options.delete(:_recall) { {} }
- if script_name && original_script_name
+ original_script_name = options.delete(:original_script_name)
+ script_name = find_script_name options
+
+ if original_script_name
script_name = original_script_name + script_name
end
- path_options = options.except(*RESERVED_OPTIONS)
- path_options = yield(path_options) if block_given?
+ path_options = options.dup
+ RESERVED_OPTIONS.each { |ro| path_options.delete ro }
- path, params = generate(path_options, recall || {})
- params.merge!(options[:params] || {})
+ path, params = generate(route_name, path_options, recall)
- ActionDispatch::Http::URL.url_for(options.merge!({
- :path => path,
- :script_name => script_name,
- :params => params,
- :user => user,
- :password => password
- }))
+ if options.key? :params
+ params.merge! options[:params]
+ end
+
+ options[:path] = path
+ options[:script_name] = script_name
+ options[:params] = params
+ options[:user] = user
+ options[:password] = password
+
+ url_strategy.call options
end
def call(env)
- @router.call(env)
+ req = request_class.new(env)
+ req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
+ @router.serve(req)
end
def recognize_path(path, environment = {})
@@ -687,8 +756,8 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- req = @request_class.new(env)
- @router.recognize(req) do |route, _matches, params|
+ req = request_class.new(env)
+ @router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
if value.is_a?(String)
@@ -696,14 +765,12 @@ module ActionDispatch
params[key] = URI.parser.unescape(value)
end
end
- old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
- env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params)
- dispatcher = route.app
- while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
- dispatcher = dispatcher.app
- end
+ old_params = req.path_parameters
+ req.path_parameters = old_params.merge params
+ app = route.app
+ if app.matches?(req) && app.dispatcher?
+ dispatcher = app.app
- if dispatcher.is_a?(Dispatcher)
if dispatcher.controller(params, false)
dispatcher.prepare_params!(params)
return params
@@ -715,17 +782,6 @@ module ActionDispatch
raise ActionController::RoutingError, "No route matches #{path.inspect}"
end
-
- private
-
- def extract_authentication(options)
- if options[:user] && options[:password]
- [options.delete(:user), options.delete(:password)]
- else
- nil
- end
- end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 4a0ef40873..eb554ec383 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -152,21 +152,26 @@ module ActionDispatch
when nil
_routes.url_for(url_options.symbolize_keys)
when Hash
- _routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
+ route_name = options.delete :use_route
+ _routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
+ route_name)
when String
options
+ when Symbol
+ HelperMethodBuilder.url.handle_string_call self, options
when Array
polymorphic_url(options, options.extract_options!)
+ when Class
+ HelperMethodBuilder.url.handle_class_call self, options
else
- polymorphic_url(options)
+ HelperMethodBuilder.url.handle_model_call self, options
end
end
protected
def optimize_routes_generation?
- return @_optimized_routes if defined?(@_optimized_routes)
- @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty?
+ _routes.optimize_routes_generation? && default_url_options.empty?
end
def _with_routes(routes)
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 68feb26936..13a72220b3 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -73,7 +73,8 @@ module ActionDispatch
if Regexp === fragment
fragment
else
- @controller._compute_redirect_to_location(fragment)
+ handle = @controller || ActionController::Redirecting
+ handle._compute_redirect_to_location(@request, fragment)
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index f1f998d932..2cf38a9c2d 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -165,7 +165,7 @@ module ActionDispatch
# ROUTES TODO: These assertions should really work in an integration context
def method_missing(selector, *args, &block)
- if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector)
+ if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector)
@controller.send(selector, *args, &block)
else
super
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 cc6b763093..192ccdb9d5 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -189,8 +189,8 @@ module ActionDispatch
# This makes app.url_for and app.foo_path available in the console
if app.respond_to?(:routes)
singleton_class.class_eval do
- include app.routes.url_helpers if app.routes.respond_to?(:url_helpers)
- include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers)
+ include app.routes.url_helpers
+ include app.routes.mounted_helpers
end
end
@@ -201,7 +201,7 @@ module ActionDispatch
@url_options ||= default_url_options.dup.tap do |url_options|
url_options.reverse_merge!(controller.url_options) if controller
- if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options)
+ if @app.respond_to?(:routes)
url_options.reverse_merge!(@app.routes.default_url_options)
end
@@ -270,12 +270,6 @@ module ActionDispatch
path = location.query ? "#{location.path}?#{location.query}" : location.path
end
- unless ActionController::Base < ActionController::Testing
- ActionController::Base.class_eval do
- include ActionController::Testing
- end
- end
-
hostname, port = host.split(':')
env = {
@@ -300,13 +294,7 @@ module ActionDispatch
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full uri
- uri = URI.parse('/')
- uri.scheme ||= env['rack.url_scheme']
- uri.host ||= env['SERVER_NAME']
- uri.port ||= env['SERVER_PORT'].try(:to_i)
- uri += path
-
- session.request(uri.to_s, env)
+ session.request(build_full_uri(path, env), env)
@request_count += 1
@request = ActionDispatch::Request.new(session.last_request.env)
@@ -319,6 +307,10 @@ module ActionDispatch
return response.status
end
+
+ def build_full_uri(path, env)
+ "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ end
end
module Runner
@@ -338,6 +330,7 @@ module ActionDispatch
xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args|
reset! unless integration_session
+ reset_template_assertion
# reset the html_document variable, but only for new get/post calls
@html_document = nil unless method == 'cookies' || method == 'assigns'
integration_session.__send__(method, *args).tap do
@@ -356,7 +349,7 @@ module ActionDispatch
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
- def open_session(app = nil)
+ def open_session
dup.tap do |session|
yield session if block_given?
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index 57c678843b..de3dc5f924 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -39,7 +39,7 @@ module ActionDispatch
end
def action=(action_name)
- path_parameters["action"] = action_name.to_s
+ path_parameters[:action] = action_name.to_s
end
def if_modified_since=(last_modified)
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 a51f6a434a..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.beta1"
- 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 b1a5044399..fc59bf19c4 100644
--- a/actionpack/test/abstract/collector_test.rb
+++ b/actionpack/test/abstract/collector_test.rb
@@ -24,15 +24,21 @@ module AbstractController
test "does not respond to unknown mime types" do
collector = MyCollector.new
- assert !collector.respond_to?(:unknown)
+ assert_not_respond_to collector, :unknown
end
test "register mime types on method missing" do
AbstractController::Collector.send(:remove_method, :js)
- collector = MyCollector.new
- assert !collector.respond_to?(:js)
- collector.js
- assert_respond_to collector, :js
+ begin
+ collector = MyCollector.new
+ assert_not_respond_to collector, :js
+ collector.js
+ assert_respond_to collector, :js
+ ensure
+ unless AbstractController::Collector.method_defined? :js
+ AbstractController::Collector.generate_method_for_mime :js
+ end
+ end
end
test "does not register unknown mime types" do
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 03a4741f42..674fb253f4 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -15,6 +15,12 @@ silence_warnings do
Encoding.default_external = "UTF-8"
end
+require 'drb'
+require 'drb/unix'
+require 'tempfile'
+
+PROCESS_COUNT = (ENV['N'] || 4).to_i
+
require 'active_support/testing/autorun'
require 'abstract_controller'
require 'action_controller'
@@ -105,6 +111,9 @@ end
module ActiveSupport
class TestCase
include ActionDispatch::DrawOnce
+ if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0
+ parallelize_me!
+ end
end
end
@@ -251,7 +260,6 @@ end
module ActionController
class Base
- include ActionController::Testing
# This stub emulates the Railtie including the URL helpers from a Rails application
include SharedTestRoutes.url_helpers
include SharedTestRoutes.mounted_helpers
@@ -306,22 +314,95 @@ end
module ActionDispatch
module RoutingVerbs
- def get(uri_or_host, path = nil)
+ def send_request(uri_or_host, method, path)
host = uri_or_host.host unless path
path ||= uri_or_host.path
params = {'PATH_INFO' => path,
- 'REQUEST_METHOD' => 'GET',
+ 'REQUEST_METHOD' => method,
'HTTP_HOST' => host}
- routes.call(params)[2].join
+ routes.call(params)
+ end
+
+ def request_path_params(path, options = {})
+ method = options[:method] || 'GET'
+ resp = send_request URI('http://localhost' + path), method.to_s.upcase, nil
+ status = resp.first
+ if status == 404
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
+ controller.request.path_parameters
+ end
+
+ def get(uri_or_host, path = nil)
+ send_request(uri_or_host, 'GET', path)[2].join
+ end
+
+ def post(uri_or_host, path = nil)
+ send_request(uri_or_host, 'POST', path)[2].join
+ end
+
+ def put(uri_or_host, path = nil)
+ send_request(uri_or_host, 'PUT', path)[2].join
+ end
+
+ def delete(uri_or_host, path = nil)
+ send_request(uri_or_host, 'DELETE', path)[2].join
+ end
+
+ def patch(uri_or_host, path = nil)
+ send_request(uri_or_host, 'PATCH', path)[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))
+ def url_for(set, options)
+ route_name = options.delete :use_route
+ set.url_for options.merge(:only_path => true), route_name
+ end
+
+ def make_set(strict = true)
+ tc = self
+ TestSet.new ->(c) { tc.controller = c }, strict
+ end
+
+ class TestSet < ActionDispatch::Routing::RouteSet
+ attr_reader :strict
+
+ def initialize(block, strict = false)
+ @block = block
+ @strict = strict
+ super()
+ end
+
+ class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller(params, default_controller=true)
+ super(params, @set.strict)
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ super if @set.strict
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
end
end
@@ -360,3 +441,76 @@ end
def jruby_skip(message = '')
skip message if defined?(JRUBY_VERSION)
end
+
+require 'mocha/setup' # FIXME: stop using mocha
+
+class ForkingExecutor
+ class Server
+ include DRb::DRbUndumped
+
+ def initialize
+ @queue = Queue.new
+ end
+
+ def record reporter, result
+ reporter.record result
+ end
+
+ def << o
+ o[2] = DRbObject.new(o[2]) if o
+ @queue << o
+ end
+ def pop; @queue.pop; end
+ end
+
+ def initialize size
+ @size = size
+ @queue = Server.new
+ file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tests', 'fd')
+ @url = "drbunix://#{file}"
+ @pool = nil
+ DRb.start_service @url, @queue
+ end
+
+ def << work; @queue << work; end
+
+ def shutdown
+ pool = @size.times.map {
+ fork {
+ DRb.stop_service
+ queue = DRbObject.new_with_uri @url
+ while job = queue.pop
+ klass = job[0]
+ method = job[1]
+ reporter = job[2]
+ result = Minitest.run_one_method klass, method
+ if result.error?
+ translate_exceptions result
+ end
+ queue.record reporter, result
+ end
+ }
+ }
+ @size.times { @queue << nil }
+ pool.each { |pid| Process.waitpid pid }
+ end
+
+ private
+ def translate_exceptions(result)
+ result.failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
+end
+
+if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0
+ # Use N processes (N defaults to 4)
+ Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT)
+end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index b6b5a218cc..311302819e 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -488,6 +488,11 @@ class AssertTemplateTest < ActionController::TestCase
assert_raise(ActiveSupport::TestCase::Assertion) do
assert_template :file => 'test/hello_world'
end
+
+ get :render_file_absolute_path
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_template file: nil
+ end
end
def test_with_nil_passes_when_no_template_rendered
@@ -612,6 +617,24 @@ class AssertTemplateTest < ActionController::TestCase
get :nothing
assert_template nil
+
+ get :partial
+ assert_template partial: 'test/_partial'
+
+ get :nothing
+ assert_template partial: nil
+
+ get :render_with_layout
+ assert_template layout: 'layouts/standard'
+
+ get :nothing
+ assert_template layout: nil
+
+ get :render_file_relative_path
+ assert_template file: 'README.rdoc'
+
+ get :nothing
+ assert_template file: nil
end
end
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
index 114bbf3c22..f07d201563 100644
--- a/actionpack/test/controller/assert_select_test.rb
+++ b/actionpack/test/controller/assert_select_test.rb
@@ -56,13 +56,16 @@ class AssertSelectTest < ActionController::TestCase
def setup
super
+ @old_delivery_method = ActionMailer::Base.delivery_method
+ @old_perform_deliveries = ActionMailer::Base.perform_deliveries
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
end
def teardown
super
+ ActionMailer::Base.delivery_method = @old_delivery_method
+ ActionMailer::Base.perform_deliveries = @old_perform_deliveries
ActionMailer::Base.deliveries.clear
end
@@ -79,13 +82,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
@@ -100,13 +103,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)/ }
@@ -124,6 +128,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"
@@ -144,29 +149,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
@@ -204,7 +209,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 57b45b8f7b..c0e6a2ebd1 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -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,23 @@ 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_fragment_cache_instrumentation
+ payload = nil
+
+ subscriber = proc do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ payload = event.payload
+ end
+
+ ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_controller") do
+ get :inline_fragment_cached
+ end
+
+ assert_equal "functional_caching", payload[:controller]
+ assert_equal "inline_fragment_cached", payload[:action]
end
def test_html_formatted_fragment_caching
@@ -228,7 +251,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 +262,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
diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb
index 03d5d27cf4..89667df3a4 100644
--- a/actionpack/test/controller/content_type_test.rb
+++ b/actionpack/test/controller/content_type_test.rb
@@ -68,12 +68,11 @@ class ContentTypeTest < ActionController::TestCase
end
def test_render_changed_charset_default
- ActionDispatch::Response.default_charset = "utf-16"
- get :render_defaults
- assert_equal "utf-16", @response.charset
- assert_equal Mime::HTML, @response.content_type
- ensure
- ActionDispatch::Response.default_charset = "utf-8"
+ with_default_charset "utf-16" do
+ get :render_defaults
+ assert_equal "utf-16", @response.charset
+ assert_equal Mime::HTML, @response.content_type
+ end
end
# :ported:
@@ -105,12 +104,11 @@ class ContentTypeTest < ActionController::TestCase
end
def test_nil_default_for_erb
- ActionDispatch::Response.default_charset = nil
- get :render_default_for_erb
- assert_equal Mime::HTML, @response.content_type
- assert_nil @response.charset, @response.headers.inspect
- ensure
- ActionDispatch::Response.default_charset = "utf-8"
+ with_default_charset nil do
+ get :render_default_for_erb
+ assert_equal Mime::HTML, @response.content_type
+ assert_nil @response.charset, @response.headers.inspect
+ end
end
def test_default_for_erb
@@ -130,6 +128,16 @@ class ContentTypeTest < ActionController::TestCase
assert_equal Mime::HTML, @response.content_type
assert_equal "utf-8", @response.charset
end
+
+ private
+
+ def with_default_charset(charset)
+ old_default_charset = ActionDispatch::Response.default_charset
+ ActionDispatch::Response.default_charset = charset
+ yield
+ ensure
+ ActionDispatch::Response.default_charset = old_default_charset
+ end
end
class AcceptBasedContentTypeTest < ActionController::TestCase
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index c87494aa64..b2b01b3fa9 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -2,13 +2,13 @@ require 'abstract_unit'
class ActionController::Base
class << self
- %w(append_around_filter prepend_after_filter prepend_around_filter prepend_before_filter skip_after_filter skip_before_filter skip_filter).each do |pending|
+ %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action skip_action_callback).each do |pending|
define_method(pending) do |*args|
$stderr.puts "#{pending} unimplemented: #{args.inspect}"
end unless method_defined?(pending)
end
- def before_filters
+ def before_actions
filters = _process_action_callbacks.select { |c| c.kind == :before }
filters.map! { |c| c.raw_filter }
end
@@ -28,8 +28,8 @@ end
class FilterTest < ActionController::TestCase
class TestController < ActionController::Base
- before_filter :ensure_login
- after_filter :clean_up
+ before_action :ensure_login
+ after_action :clean_up
def show
render :inline => "ran action"
@@ -42,13 +42,13 @@ class FilterTest < ActionController::TestCase
end
def clean_up
- @ran_after_filter ||= []
- @ran_after_filter << "clean_up"
+ @ran_after_action ||= []
+ @ran_after_action << "clean_up"
end
end
class ChangingTheRequirementsController < TestController
- before_filter :ensure_login, :except => [:go_wild]
+ before_action :ensure_login, :except => [:go_wild]
def go_wild
render :text => "gobble"
@@ -56,9 +56,9 @@ class FilterTest < ActionController::TestCase
end
class TestMultipleFiltersController < ActionController::Base
- before_filter :try_1
- before_filter :try_2
- before_filter :try_3
+ before_action :try_1
+ before_action :try_2
+ before_action :try_3
(1..3).each do |i|
define_method "fail_#{i}" do
@@ -78,8 +78,8 @@ class FilterTest < ActionController::TestCase
end
class RenderingController < ActionController::Base
- before_filter :before_filter_rendering
- after_filter :unreached_after_filter
+ before_action :before_action_rendering
+ after_action :unreached_after_action
def show
@ran_action = true
@@ -87,29 +87,29 @@ class FilterTest < ActionController::TestCase
end
private
- def before_filter_rendering
+ def before_action_rendering
@ran_filter ||= []
- @ran_filter << "before_filter_rendering"
+ @ran_filter << "before_action_rendering"
render :inline => "something else"
end
- def unreached_after_filter
- @ran_filter << "unreached_after_filter_after_render"
+ def unreached_after_action
+ @ran_filter << "unreached_after_action_after_render"
end
end
- class RenderingForPrependAfterFilterController < RenderingController
- prepend_after_filter :unreached_prepend_after_filter
+ class RenderingForPrependAfterActionController < RenderingController
+ prepend_after_action :unreached_prepend_after_action
private
- def unreached_prepend_after_filter
- @ran_filter << "unreached_preprend_after_filter_after_render"
+ def unreached_prepend_after_action
+ @ran_filter << "unreached_preprend_after_action_after_render"
end
end
- class BeforeFilterRedirectionController < ActionController::Base
- before_filter :before_filter_redirects
- after_filter :unreached_after_filter
+ class BeforeActionRedirectionController < ActionController::Base
+ before_action :before_action_redirects
+ after_action :unreached_after_action
def show
@ran_action = true
@@ -122,23 +122,23 @@ class FilterTest < ActionController::TestCase
end
private
- def before_filter_redirects
+ def before_action_redirects
@ran_filter ||= []
- @ran_filter << "before_filter_redirects"
+ @ran_filter << "before_action_redirects"
redirect_to(:action => 'target_of_redirection')
end
- def unreached_after_filter
- @ran_filter << "unreached_after_filter_after_redirection"
+ def unreached_after_action
+ @ran_filter << "unreached_after_action_after_redirection"
end
end
- class BeforeFilterRedirectionForPrependAfterFilterController < BeforeFilterRedirectionController
- prepend_after_filter :unreached_prepend_after_filter_after_redirection
+ class BeforeActionRedirectionForPrependAfterActionController < BeforeActionRedirectionController
+ prepend_after_action :unreached_prepend_after_action_after_redirection
private
- def unreached_prepend_after_filter_after_redirection
- @ran_filter << "unreached_prepend_after_filter_after_redirection"
+ def unreached_prepend_after_action_after_redirection
+ @ran_filter << "unreached_prepend_after_action_after_redirection"
end
end
@@ -151,8 +151,8 @@ class FilterTest < ActionController::TestCase
render :inline => "ran action"
end
- def show_without_filter
- render :inline => "ran action without filter"
+ def show_without_action
+ render :inline => "ran action without action"
end
private
@@ -168,70 +168,70 @@ class FilterTest < ActionController::TestCase
end
class ConditionalCollectionFilterController < ConditionalFilterController
- before_filter :ensure_login, :except => [ :show_without_filter, :another_action ]
+ before_action :ensure_login, :except => [ :show_without_action, :another_action ]
end
class OnlyConditionSymController < ConditionalFilterController
- before_filter :ensure_login, :only => :show
+ before_action :ensure_login, :only => :show
end
class ExceptConditionSymController < ConditionalFilterController
- before_filter :ensure_login, :except => :show_without_filter
+ before_action :ensure_login, :except => :show_without_action
end
class BeforeAndAfterConditionController < ConditionalFilterController
- before_filter :ensure_login, :only => :show
- after_filter :clean_up_tmp, :only => :show
+ before_action :ensure_login, :only => :show
+ after_action :clean_up_tmp, :only => :show
end
class OnlyConditionProcController < ConditionalFilterController
- before_filter(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_filter", true) }
+ before_action(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class ExceptConditionProcController < ConditionalFilterController
- before_filter(:except => :show_without_filter) {|c| c.instance_variable_set(:"@ran_proc_filter", true) }
+ before_action(:except => :show_without_action) {|c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class ConditionalClassFilter
- def self.before(controller) controller.instance_variable_set(:"@ran_class_filter", true) end
+ def self.before(controller) controller.instance_variable_set(:"@ran_class_action", true) end
end
class OnlyConditionClassController < ConditionalFilterController
- before_filter ConditionalClassFilter, :only => :show
+ before_action ConditionalClassFilter, :only => :show
end
class ExceptConditionClassController < ConditionalFilterController
- before_filter ConditionalClassFilter, :except => :show_without_filter
+ before_action ConditionalClassFilter, :except => :show_without_action
end
class AnomolousYetValidConditionController < ConditionalFilterController
- 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)}
+ before_action(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_action1", true)}, :except => :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true)}
end
class OnlyConditionalOptionsFilter < ConditionalFilterController
- before_filter :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
+ before_action :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 }
+ before_action :ensure_login, :if => Proc.new { |c| true }
+ before_action :clean_up_tmp, :if => Proc.new { |c| false }
end
class ConditionalOptionsSkipFilter < ConditionalFilterController
- before_filter :ensure_login
- before_filter :clean_up_tmp
+ before_action :ensure_login
+ before_action :clean_up_tmp
- skip_before_filter :ensure_login, if: -> { false }
- skip_before_filter :clean_up_tmp, if: -> { true }
+ skip_before_action :ensure_login, if: -> { false }
+ skip_before_action :clean_up_tmp, if: -> { true }
end
class ClassController < ConditionalFilterController
- before_filter ConditionalClassFilter
+ before_action ConditionalClassFilter
end
class PrependingController < TestController
- prepend_before_filter :wonderful_life
- # skip_before_filter :fire_flash
+ prepend_before_action :wonderful_life
+ # skip_before_action :fire_flash
private
def wonderful_life
@@ -241,8 +241,8 @@ class FilterTest < ActionController::TestCase
end
class SkippingAndLimitedController < TestController
- skip_before_filter :ensure_login
- before_filter :ensure_login, :only => :index
+ skip_before_action :ensure_login
+ before_action :ensure_login, :only => :index
def index
render :text => 'ok'
@@ -254,9 +254,9 @@ class FilterTest < ActionController::TestCase
end
class SkippingAndReorderingController < TestController
- skip_before_filter :ensure_login
- before_filter :find_record
- before_filter :ensure_login
+ skip_before_action :ensure_login
+ before_action :find_record
+ before_action :ensure_login
def index
render :text => 'ok'
@@ -270,10 +270,10 @@ class FilterTest < ActionController::TestCase
end
class ConditionalSkippingController < TestController
- skip_before_filter :ensure_login, :only => [ :login ]
- skip_after_filter :clean_up, :only => [ :login ]
+ skip_before_action :ensure_login, :only => [ :login ]
+ skip_after_action :clean_up, :only => [ :login ]
- before_filter :find_user, :only => [ :change_password ]
+ before_action :find_user, :only => [ :change_password ]
def login
render :inline => "ran action"
@@ -291,8 +291,8 @@ class FilterTest < ActionController::TestCase
end
class ConditionalParentOfConditionalSkippingController < ConditionalFilterController
- before_filter :conditional_in_parent_before, :only => [:show, :another_action]
- after_filter :conditional_in_parent_after, :only => [:show, :another_action]
+ before_action :conditional_in_parent_before, :only => [:show, :another_action]
+ after_action :conditional_in_parent_after, :only => [:show, :another_action]
private
@@ -308,20 +308,20 @@ class FilterTest < ActionController::TestCase
end
class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
- skip_before_filter :conditional_in_parent_before, :only => :another_action
- skip_after_filter :conditional_in_parent_after, :only => :another_action
+ skip_before_action :conditional_in_parent_before, :only => :another_action
+ skip_after_action :conditional_in_parent_after, :only => :another_action
end
class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
- skip_before_filter :conditional_in_parent_before, :only => :show
+ skip_before_action :conditional_in_parent_before, :only => :show
end
class ProcController < PrependingController
- before_filter(proc { |c| c.instance_variable_set(:"@ran_proc_filter", true) })
+ before_action(proc { |c| c.instance_variable_set(:"@ran_proc_action", true) })
end
class ImplicitProcController < PrependingController
- before_filter { |c| c.instance_variable_set(:"@ran_proc_filter", true) }
+ before_action { |c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class AuditFilter
@@ -367,7 +367,7 @@ class FilterTest < ActionController::TestCase
end
class AuditController < ActionController::Base
- before_filter(AuditFilter)
+ before_action(AuditFilter)
def show
render :text => "hello"
@@ -375,14 +375,14 @@ class FilterTest < ActionController::TestCase
end
class AroundFilterController < PrependingController
- around_filter AroundFilter.new
+ around_action AroundFilter.new
end
class BeforeAfterClassFilterController < PrependingController
begin
filter = AroundFilter.new
- before_filter filter
- after_filter filter
+ before_action filter
+ after_action filter
end
end
@@ -394,18 +394,18 @@ class FilterTest < ActionController::TestCase
super()
end
- before_filter { |c| c.class.execution_log << " before procfilter " }
- prepend_around_filter AroundFilter.new
+ before_action { |c| c.class.execution_log << " before procfilter " }
+ prepend_around_action AroundFilter.new
- after_filter { |c| c.class.execution_log << " after procfilter " }
- append_around_filter AppendedAroundFilter.new
+ after_action { |c| c.class.execution_log << " after procfilter " }
+ append_around_action AppendedAroundFilter.new
end
class MixedSpecializationController < ActionController::Base
class OutOfOrder < StandardError; end
- before_filter :first
- before_filter :second, :only => :foo
+ before_action :first
+ before_action :second, :only => :foo
def foo
render :text => 'foo'
@@ -426,7 +426,7 @@ class FilterTest < ActionController::TestCase
end
class DynamicDispatchController < ActionController::Base
- before_filter :choose
+ before_action :choose
%w(foo bar baz).each do |action|
define_method(action) { render :text => action }
@@ -439,9 +439,9 @@ class FilterTest < ActionController::TestCase
end
class PrependingBeforeAndAfterController < ActionController::Base
- prepend_before_filter :before_all
- prepend_after_filter :after_all
- before_filter :between_before_all_and_after_all
+ prepend_before_action :before_all
+ prepend_after_action :after_all
+ before_action :between_before_all_and_after_all
def before_all
@ran_filter ||= []
@@ -473,7 +473,7 @@ class FilterTest < ActionController::TestCase
end
class RescuedController < ActionController::Base
- around_filter RescuingAroundFilterWithBlock.new
+ around_action RescuingAroundFilterWithBlock.new
def show
raise ErrorToRescue.new("Something made the bad noise.")
@@ -482,10 +482,10 @@ class FilterTest < ActionController::TestCase
class NonYieldingAroundFilterController < ActionController::Base
- before_filter :filter_one
- around_filter :non_yielding_filter
- before_filter :filter_two
- after_filter :filter_three
+ before_action :filter_one
+ around_action :non_yielding_action
+ before_action :action_two
+ after_action :action_three
def index
render :inline => "index"
@@ -498,24 +498,24 @@ class FilterTest < ActionController::TestCase
@filters << "filter_one"
end
- def filter_two
- @filters << "filter_two"
+ def action_two
+ @filters << "action_two"
end
- def non_yielding_filter
+ def non_yielding_action
@filters << "it didn't yield"
@filter_return_value
end
- def filter_three
- @filters << "filter_three"
+ def action_three
+ @filters << "action_three"
end
end
class ImplicitActionsController < ActionController::Base
- before_filter :find_only, :only => :edit
- before_filter :find_except, :except => :edit
+ before_action :find_only, :only => :edit
+ before_action :find_except, :except => :edit
private
@@ -528,7 +528,7 @@ class FilterTest < ActionController::TestCase
end
end
- def test_non_yielding_around_filters_not_returning_false_do_not_raise
+ def test_non_yielding_around_actions_not_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
assert_nothing_raised do
@@ -536,7 +536,7 @@ class FilterTest < ActionController::TestCase
end
end
- def test_non_yielding_around_filters_returning_false_do_not_raise
+ def test_non_yielding_around_actions_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", false
assert_nothing_raised do
@@ -544,64 +544,64 @@ class FilterTest < ActionController::TestCase
end
end
- def test_after_filters_are_not_run_if_around_filter_returns_false
+ def test_after_actions_are_not_run_if_around_action_returns_false
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", false
test_process(controller, "index")
assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters']
end
- def test_after_filters_are_not_run_if_around_filter_does_not_yield
+ def test_after_actions_are_not_run_if_around_action_does_not_yield
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
test_process(controller, "index")
assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters']
end
- def test_added_filter_to_inheritance_graph
- assert_equal [ :ensure_login ], TestController.before_filters
+ def test_added_action_to_inheritance_graph
+ assert_equal [ :ensure_login ], TestController.before_actions
end
def test_base_class_in_isolation
- assert_equal [ ], ActionController::Base.before_filters
+ assert_equal [ ], ActionController::Base.before_actions
end
- def test_prepending_filter
- assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_filters
+ def test_prepending_action
+ assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_actions
end
- def test_running_filters
+ def test_running_actions
test_process(PrependingController)
assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"]
end
- def test_running_filters_with_proc
+ def test_running_actions_with_proc
test_process(ProcController)
- assert assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
end
- def test_running_filters_with_implicit_proc
+ def test_running_actions_with_implicit_proc
test_process(ImplicitProcController)
- assert assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
end
- def test_running_filters_with_class
+ def test_running_actions_with_class
test_process(AuditController)
assert assigns["was_audited"]
end
- def test_running_anomolous_yet_valid_condition_filters
+ def test_running_anomolous_yet_valid_condition_actions
test_process(AnomolousYetValidConditionController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- assert assigns["ran_class_filter"]
- assert assigns["ran_proc_filter1"]
- assert assigns["ran_proc_filter2"]
+ assert assigns["ran_class_action"]
+ assert assigns["ran_proc_action1"]
+ assert assigns["ran_proc_action2"]
- test_process(AnomolousYetValidConditionController, "show_without_filter")
+ test_process(AnomolousYetValidConditionController, "show_without_action")
assert_nil assigns["ran_filter"]
- assert !assigns["ran_class_filter"]
- assert !assigns["ran_proc_filter1"]
- assert !assigns["ran_proc_filter2"]
+ assert !assigns["ran_class_action"]
+ assert !assigns["ran_proc_action1"]
+ assert !assigns["ran_proc_action2"]
end
def test_running_conditional_options
@@ -614,59 +614,59 @@ class FilterTest < ActionController::TestCase
assert_equal %w( ensure_login ), assigns["ran_filter"]
end
- def test_skipping_class_filters
+ def test_skipping_class_actions
test_process(ClassController)
- assert_equal true, assigns["ran_class_filter"]
+ assert_equal true, assigns["ran_class_action"]
skipping_class_controller = Class.new(ClassController) do
- skip_before_filter ConditionalClassFilter
+ skip_before_action ConditionalClassFilter
end
test_process(skipping_class_controller)
- assert_nil assigns['ran_class_filter']
+ assert_nil assigns['ran_class_action']
end
- def test_running_collection_condition_filters
+ def test_running_collection_condition_actions
test_process(ConditionalCollectionFilterController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- test_process(ConditionalCollectionFilterController, "show_without_filter")
+ test_process(ConditionalCollectionFilterController, "show_without_action")
assert_nil assigns["ran_filter"]
test_process(ConditionalCollectionFilterController, "another_action")
assert_nil assigns["ran_filter"]
end
- def test_running_only_condition_filters
+ def test_running_only_condition_actions
test_process(OnlyConditionSymController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- test_process(OnlyConditionSymController, "show_without_filter")
+ test_process(OnlyConditionSymController, "show_without_action")
assert_nil assigns["ran_filter"]
test_process(OnlyConditionProcController)
- assert assigns["ran_proc_filter"]
- test_process(OnlyConditionProcController, "show_without_filter")
- assert !assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
+ test_process(OnlyConditionProcController, "show_without_action")
+ assert !assigns["ran_proc_action"]
test_process(OnlyConditionClassController)
- assert assigns["ran_class_filter"]
- test_process(OnlyConditionClassController, "show_without_filter")
- assert !assigns["ran_class_filter"]
+ assert assigns["ran_class_action"]
+ test_process(OnlyConditionClassController, "show_without_action")
+ assert !assigns["ran_class_action"]
end
- def test_running_except_condition_filters
+ def test_running_except_condition_actions
test_process(ExceptConditionSymController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- test_process(ExceptConditionSymController, "show_without_filter")
+ test_process(ExceptConditionSymController, "show_without_action")
assert_nil assigns["ran_filter"]
test_process(ExceptConditionProcController)
- assert assigns["ran_proc_filter"]
- test_process(ExceptConditionProcController, "show_without_filter")
- assert !assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
+ test_process(ExceptConditionProcController, "show_without_action")
+ assert !assigns["ran_proc_action"]
test_process(ExceptConditionClassController)
- assert assigns["ran_class_filter"]
- test_process(ExceptConditionClassController, "show_without_filter")
- assert !assigns["ran_class_filter"]
+ assert assigns["ran_class_action"]
+ test_process(ExceptConditionClassController, "show_without_action")
+ assert !assigns["ran_class_action"]
end
def test_running_only_condition_and_conditional_options
@@ -674,70 +674,70 @@ class FilterTest < ActionController::TestCase
assert_not assigns["ran_conditional_index_proc"]
end
- def test_running_before_and_after_condition_filters
+ def test_running_before_and_after_condition_actions
test_process(BeforeAndAfterConditionController)
assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"]
- test_process(BeforeAndAfterConditionController, "show_without_filter")
+ test_process(BeforeAndAfterConditionController, "show_without_action")
assert_nil assigns["ran_filter"]
end
- def test_around_filter
+ def test_around_action
test_process(AroundFilterController)
assert assigns["before_ran"]
assert assigns["after_ran"]
end
- def test_before_after_class_filter
+ def test_before_after_class_action
test_process(BeforeAfterClassFilterController)
assert assigns["before_ran"]
assert assigns["after_ran"]
end
- def test_having_properties_in_around_filter
+ def test_having_properties_in_around_action
test_process(AroundFilterController)
assert_equal "before and after", assigns["execution_log"]
end
- def test_prepending_and_appending_around_filter
+ def test_prepending_and_appending_around_action
test_process(MixedFilterController)
assert_equal " before aroundfilter before procfilter before appended aroundfilter " +
" after appended aroundfilter after procfilter after aroundfilter ",
MixedFilterController.execution_log
end
- def test_rendering_breaks_filtering_chain
+ def test_rendering_breaks_actioning_chain
response = test_process(RenderingController)
assert_equal "something else", response.body
assert !assigns["ran_action"]
end
- def test_before_filter_rendering_breaks_filtering_chain_for_after_filter
+ def test_before_action_rendering_breaks_actioning_chain_for_after_action
test_process(RenderingController)
- assert_equal %w( before_filter_rendering ), assigns["ran_filter"]
+ assert_equal %w( before_action_rendering ), assigns["ran_filter"]
assert !assigns["ran_action"]
end
- def test_before_filter_redirects_breaks_filtering_chain_for_after_filter
- test_process(BeforeFilterRedirectionController)
+ def test_before_action_redirects_breaks_actioning_chain_for_after_action
+ test_process(BeforeActionRedirectionController)
assert_response :redirect
- assert_equal "http://test.host/filter_test/before_filter_redirection/target_of_redirection", redirect_to_url
- assert_equal %w( before_filter_redirects ), assigns["ran_filter"]
+ assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url
+ assert_equal %w( before_action_redirects ), assigns["ran_filter"]
end
- def test_before_filter_rendering_breaks_filtering_chain_for_preprend_after_filter
- test_process(RenderingForPrependAfterFilterController)
- assert_equal %w( before_filter_rendering ), assigns["ran_filter"]
+ def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action
+ test_process(RenderingForPrependAfterActionController)
+ assert_equal %w( before_action_rendering ), assigns["ran_filter"]
assert !assigns["ran_action"]
end
- def test_before_filter_redirects_breaks_filtering_chain_for_preprend_after_filter
- test_process(BeforeFilterRedirectionForPrependAfterFilterController)
+ def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action
+ test_process(BeforeActionRedirectionForPrependAfterActionController)
assert_response :redirect
- assert_equal "http://test.host/filter_test/before_filter_redirection_for_prepend_after_filter/target_of_redirection", redirect_to_url
- assert_equal %w( before_filter_redirects ), assigns["ran_filter"]
+ assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url
+ assert_equal %w( before_action_redirects ), assigns["ran_filter"]
end
- def test_filters_with_mixed_specialization_run_in_order
+ def test_actions_with_mixed_specialization_run_in_order
assert_nothing_raised do
response = test_process(MixedSpecializationController, 'bar')
assert_equal 'bar', response.body
@@ -758,7 +758,7 @@ class FilterTest < ActionController::TestCase
end
end
- def test_running_prepended_before_and_after_filter
+ def test_running_prepended_before_and_after_action
test_process(PrependingBeforeAndAfterController)
assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"]
end
@@ -775,26 +775,26 @@ class FilterTest < ActionController::TestCase
assert_equal %w( find_record ensure_login ), assigns["ran_filter"]
end
- def test_conditional_skipping_of_filters
+ def test_conditional_skipping_of_actions
test_process(ConditionalSkippingController, "login")
assert_nil assigns["ran_filter"]
test_process(ConditionalSkippingController, "change_password")
assert_equal %w( ensure_login find_user ), assigns["ran_filter"]
test_process(ConditionalSkippingController, "login")
- assert !@controller.instance_variable_defined?("@ran_after_filter")
+ assert !@controller.instance_variable_defined?("@ran_after_action")
test_process(ConditionalSkippingController, "change_password")
- assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_filter")
+ assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action")
end
- def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional
+ def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional
test_process(ChildOfConditionalParentController)
assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter']
test_process(ChildOfConditionalParentController, 'another_action')
assert_nil assigns['ran_filter']
end
- def test_condition_skipping_of_filters_when_siblings_also_have_conditions
+ def test_condition_skipping_of_actions_when_siblings_also_have_conditions
test_process(ChildOfConditionalParentController)
assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter']
test_process(AnotherChildOfConditionalParentController)
@@ -808,7 +808,7 @@ class FilterTest < ActionController::TestCase
assert_nil assigns['ran_filter']
end
- def test_a_rescuing_around_filter
+ def test_a_rescuing_around_action
response = nil
assert_nothing_raised do
response = test_process(RescuedController)
@@ -818,7 +818,7 @@ class FilterTest < ActionController::TestCase
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
end
- def test_filters_obey_only_and_except_for_implicit_actions
+ def test_actions_obey_only_and_except_for_implicit_actions
test_process(ImplicitActionsController, 'show')
assert_equal 'Except', assigns(:except)
assert_nil assigns(:only)
@@ -852,7 +852,7 @@ class PostsController < ActionController::Base
include AroundExceptions
end
- module_eval %w(raises_before raises_after raises_both no_raise no_filter).map { |action| "def #{action}; default_action end" }.join("\n")
+ module_eval %w(raises_before raises_after raises_both no_raise no_action).map { |action| "def #{action}; default_action end" }.join("\n")
private
def default_action
@@ -861,9 +861,9 @@ class PostsController < ActionController::Base
end
class ControllerWithSymbolAsFilter < PostsController
- around_filter :raise_before, :only => :raises_before
- around_filter :raise_after, :only => :raises_after
- around_filter :without_exception, :only => :no_raise
+ around_action :raise_before, :only => :raises_before
+ around_action :raise_after, :only => :raises_after
+ around_action :without_exception, :only => :no_raise
private
def raise_before
@@ -895,7 +895,7 @@ class ControllerWithFilterClass < PostsController
end
end
- around_filter YieldingFilter, :only => :raises_after
+ around_action YieldingFilter, :only => :raises_after
end
class ControllerWithFilterInstance < PostsController
@@ -906,11 +906,11 @@ class ControllerWithFilterInstance < PostsController
end
end
- around_filter YieldingFilter.new, :only => :raises_after
+ around_action YieldingFilter.new, :only => :raises_after
end
class ControllerWithProcFilter < PostsController
- around_filter(:only => :no_raise) do |c,b|
+ around_action(:only => :no_raise) do |c,b|
c.instance_variable_set(:"@before", true)
b.call
c.instance_variable_set(:"@after", true)
@@ -918,14 +918,14 @@ class ControllerWithProcFilter < PostsController
end
class ControllerWithNestedFilters < ControllerWithSymbolAsFilter
- around_filter :raise_before, :raise_after, :without_exception, :only => :raises_both
+ around_action :raise_before, :raise_after, :without_exception, :only => :raises_both
end
class ControllerWithAllTypesOfFilters < PostsController
- before_filter :before
- around_filter :around
- after_filter :after
- around_filter :around_again
+ before_action :before
+ around_action :around
+ after_action :after
+ around_action :around_again
private
def before
@@ -951,8 +951,8 @@ class ControllerWithAllTypesOfFilters < PostsController
end
class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
- skip_filter :around_again
- skip_filter :after
+ skip_action_callback :around_again
+ skip_action_callback :after
end
class YieldingAroundFiltersTest < ActionController::TestCase
@@ -963,7 +963,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase
assert_nothing_raised { test_process(controller,'no_raise') }
assert_nothing_raised { test_process(controller,'raises_before') }
assert_nothing_raised { test_process(controller,'raises_after') }
- assert_nothing_raised { test_process(controller,'no_filter') }
+ assert_nothing_raised { test_process(controller,'no_action') }
end
def test_with_symbol
@@ -992,7 +992,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase
assert assigns['after']
end
- def test_nested_filters
+ def test_nested_actions
controller = ControllerWithNestedFilters
assert_nothing_raised do
begin
@@ -1008,31 +1008,31 @@ class YieldingAroundFiltersTest < ActionController::TestCase
end
end
- def test_filter_order_with_all_filter_types
+ def test_action_order_with_all_action_types
test_process(ControllerWithAllTypesOfFilters,'no_raise')
assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', assigns['ran_filter'].join(' ')
end
- def test_filter_order_with_skip_filter_method
+ def test_action_order_with_skip_action_method
test_process(ControllerWithTwoLessFilters,'no_raise')
assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ')
end
- def test_first_filter_in_multiple_before_filter_chain_halts
+ def test_first_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
response = test_process(controller, 'fail_1')
assert_equal ' ', response.body
assert_equal 1, controller.instance_variable_get(:@try)
end
- def test_second_filter_in_multiple_before_filter_chain_halts
+ def test_second_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
response = test_process(controller, 'fail_2')
assert_equal ' ', response.body
assert_equal 2, controller.instance_variable_get(:@try)
end
- def test_last_filter_in_multiple_before_filter_chain_halts
+ def test_last_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
response = test_process(controller, 'fail_3')
assert_equal ' ', response.body
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index 25a4857eba..3720a920d0 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -210,20 +210,29 @@ 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
- class SubclassesTestController < TestController; end
-
def test_add_flash_type_to_subclasses
- TestController.add_flash_types :foo
- assert SubclassesTestController._flash_types.include?(:foo)
+ 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_do_not_add_flash_type_to_parent_class
- SubclassesTestController.add_flash_types :bar
+ 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/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb
index 90548d4294..9052fc6962 100644
--- a/actionpack/test/controller/http_basic_authentication_test.rb
+++ b/actionpack/test/controller/http_basic_authentication_test.rb
@@ -129,6 +129,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
assert_response :unauthorized
end
+ test "authentication request with wrong scheme" do
+ header = 'Bearer ' + encode_credentials('David', 'Goliath').split(' ', 2)[1]
+ @request.env['HTTP_AUTHORIZATION'] = header
+ get :search
+ assert_response :unauthorized
+ end
+
private
def encode_credentials(username, password)
diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb
index 86b94652ce..8c6c8a0aa7 100644
--- a/actionpack/test/controller/http_token_authentication_test.rb
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -132,13 +132,50 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
assert_equal(expected, actual)
end
- private
+ test "token_and_options returns empty string with empty token" do
+ token = ''
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
- def sample_request(token)
- @sample_request ||= OpenStruct.new authorization: %{Token token="#{token}"}
+ test "token_and_options returns correct token with nounce option" do
+ token = "rcHu+HzSFw89Ypyhn/896A="
+ nonce_hash = {nonce: "123abc"}
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash))
+ expected_token = token
+ expected_nonce = {"nonce" => nonce_hash[:nonce]}
+ assert_equal(expected_token, actual.first)
+ assert_equal(expected_nonce, actual.last)
end
- def encode_credentials(token, options = {})
- ActionController::HttpAuthentication::Token.encode_credentials(token, options)
+ test "token_and_options returns nil with no value after the equal sign" do
+ actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first
+ expected = nil
+ assert_equal(expected, actual)
+ end
+
+ test "raw_params returns a tuple of two key value pair strings" do
+ auth = sample_request("rcHu+HzSFw89Ypyhn/896A=").authorization.to_s
+ actual = ActionController::HttpAuthentication::Token.raw_params(auth)
+ expected = ["token=\"rcHu+HzSFw89Ypyhn/896A=\"", "nonce=\"def\""]
+ assert_equal(expected, actual)
end
+
+ private
+
+ def sample_request(token, options = {nonce: "def"})
+ authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)|
+ arr << "#{k}=\"#{v}\""
+ end.join(", ")
+ @sample_request ||= OpenStruct.new authorization: authorization
+ end
+
+ def malformed_request
+ @malformed_request ||= OpenStruct.new authorization: %{Token token=}
+ end
+
+ def encode_credentials(token, options = {})
+ ActionController::HttpAuthentication::Token.encode_credentials(token, options)
+ end
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index e851cc6a63..78cce3aa64 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'controller/fake_controllers'
require 'action_view/vendor/html-scanner'
+require 'rails/engine'
class SessionTest < ActiveSupport::TestCase
StubApp = lambda { |env|
@@ -374,6 +375,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
follow_redirect!
assert_response :success
assert_equal "/get", path
+
+ get '/moved'
+ assert_response :redirect
+ assert_redirected_to '/method'
end
end
@@ -511,6 +516,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
set.draw do
+ get 'moved' => redirect('/method')
+
match ':action', :to => controller, :via => [:get, :post], :as => :action
get 'get/:action', :to => controller, :as => :get_action
end
@@ -589,7 +596,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
@routes ||= ActionDispatch::Routing::RouteSet.new
end
- class MountedApp
+ class MountedApp < Rails::Engine
def self.routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
@@ -769,3 +776,34 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
assert_equal "/foo/1/edit", url_for(:action => 'edit', :only_path => true)
end
end
+
+class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def status
+ head :ok
+ end
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def app
+ self.class
+ end
+
+ routes.draw do
+ get "/foo/status" => 'head_with_status_action_integration_test/foo#status'
+ end
+
+ test "get /foo/status with head result does not cause stack overflow error" do
+ assert_nothing_raised do
+ get '/foo/status'
+ end
+ assert_response :ok
+ end
+end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 0a431270b5..0500b7c789 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'active_support/concurrency/latch'
+Thread.abort_on_exception = true
module ActionController
class SSETest < ActionController::TestCase
@@ -38,14 +39,19 @@ module ActionController
ensure
sse.close
end
+
+ def sse_with_multiple_line_message
+ sse = SSE.new(response.stream)
+ sse.write("first line.\nsecond line.")
+ ensure
+ sse.close
+ end
end
tests SSETestController
def wait_for_response_stream_close
- while !response.stream.closed?
- sleep 0.01
- end
+ response.body
end
def test_basic_sse
@@ -88,9 +94,21 @@ module ActionController
assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
assert_match(/id: 2/, second_response)
end
+
+ def test_sse_with_multiple_line_message
+ get :sse_with_multiple_line_message
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n")
+ assert_match(/data: first line/, first_response)
+ assert_match(/data: second line/, second_response)
+ end
end
class LiveStreamTest < ActionController::TestCase
+ class Exception < StandardError
+ end
+
class TestController < ActionController::Base
include ActionController::Live
@@ -100,6 +118,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
@@ -145,6 +169,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'
@@ -153,32 +182,66 @@ 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
+ def overfill_buffer_and_die
+ # Write until the buffer is full. It doesn't expose that
+ # information directly, so we must hard-code its size:
+ 10.times do
+ response.stream.write '.'
+ end
+ # .. plus one more, because the #each frees up a slot:
+ response.stream.write '.'
+
+ latch.release
- class TestResponse < Live::Response
- def recycle!
- initialize
+ # This write will block, and eventually raise
+ response.stream.write 'x'
+
+ 20.times do
+ response.stream.write '.'
+ end
end
- end
- def build_response
- TestResponse.new
+ def ignore_client_disconnect
+ response.stream.ignore_disconnect = true
+
+ response.stream.write '' # commit
+
+ # These writes will be ignored
+ 15.times do
+ response.stream.write 'x'
+ end
+
+ logger.info 'Work complete'
+ latch.release
+ end
end
+ tests TestController
+
def assert_stream_closed
assert response.stream.closed?, 'stream should be closed'
+ assert response.sent?, 'stream should be sent'
end
def capture_log_output
@@ -192,6 +255,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)
@@ -213,6 +283,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
@@ -226,6 +297,62 @@ module ActionController
assert t.join(3), 'timeout expired before the thread terminated'
end
+ def test_abort_with_full_buffer
+ @controller.latch = ActiveSupport::Concurrency::Latch.new
+
+ @request.parameters[:format] = 'plain'
+ @controller.request = @request
+ @controller.response = @response
+
+ got_error = ActiveSupport::Concurrency::Latch.new
+ @response.stream.on_error do
+ ActionController::Base.logger.warn 'Error while streaming'
+ got_error.release
+ end
+
+ t = Thread.new(@response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do |part|
+ @controller.latch.await
+ body.close
+ break
+ end
+ }
+
+ capture_log_output do |output|
+ @controller.process :overfill_buffer_and_die
+ t.join
+ got_error.await
+ assert_match 'Error while streaming', output.rewind && output.read
+ end
+ end
+
+ def test_ignore_client_disconnect
+ @controller.latch = ActiveSupport::Concurrency::Latch.new
+
+ @controller.request = @request
+ @controller.response = @response
+
+ t = Thread.new(@response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do |part|
+ body.close
+ break
+ end
+ }
+
+ capture_log_output do |output|
+ @controller.process :ignore_client_disconnect
+ t.join
+ Timeout.timeout(3) do
+ @controller.latch.await
+ end
+ assert_match 'Work complete', output.rewind && output.read
+ end
+ end
+
def test_thread_locals_get_copied
@controller.tc = self
Thread.current[:originating_thread] = Thread.current.object_id
@@ -249,24 +376,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
@@ -275,7 +412,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
@@ -295,4 +444,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 c95ef8a0c7..27871ef351 100644
--- a/actionpack/test/controller/localized_templates_test.rb
+++ b/actionpack/test/controller/localized_templates_test.rb
@@ -8,22 +8,24 @@ end
class LocalizedTemplatesTest < ActionController::TestCase
tests LocalizedController
+ setup do
+ @old_locale = I18n.locale
+ end
+
+ teardown do
+ I18n.locale = @old_locale
+ end
+
def test_localized_template_is_used
- old_locale = I18n.locale
I18n.locale = :de
get :hello_world
assert_equal "Gutten Tag", @response.body
- ensure
- I18n.locale = old_locale
end
def test_default_locale_template_is_used_when_locale_is_missing
- old_locale = I18n.locale
I18n.locale = :dk
get :hello_world
assert_equal "Hello World", @response.body
- ensure
- I18n.locale = old_locale
end
def test_use_fallback_locales
@@ -36,13 +38,9 @@ class LocalizedTemplatesTest < ActionController::TestCase
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 18037b3d2f..49be7caf38 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -85,7 +85,7 @@ class ACLogSubscriberTest < ActionController::TestCase
@old_logger = ActionController::Base.logger
- @cache_path = File.expand_path('../temp/test_cache', File.dirname(__FILE__))
+ @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache')
@controller.cache_store = :file_store, @cache_path
ActionController::LogSubscriber.attach_to :action_controller
end
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
index c03c7edeb8..811c507af2 100644
--- a/actionpack/test/controller/mime/accept_format_test.rb
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -9,8 +9,6 @@ class StarStarMimeController < ActionController::Base
end
class StarStarMimeControllerTest < ActionController::TestCase
- tests StarStarMimeController
-
def test_javascript_with_format
@request.accept = "text/javascript"
get :index, :format => 'js'
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
index 499c62cc35..1bc7ad3015 100644
--- a/actionpack/test/controller/mime/respond_to_test.rb
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -128,6 +128,12 @@ class RespondToController < ActionController::Base
end
end
+ def json_with_callback
+ respond_to do |type|
+ type.json { render :json => 'JS', :callback => 'alert' }
+ end
+ end
+
def iphone_with_html_response_type
request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
@@ -258,8 +264,6 @@ class RespondToController < ActionController::Base
end
class RespondToControllerTest < ActionController::TestCase
- tests RespondToController
-
def setup
super
@request.host = "www.example.com"
@@ -492,6 +496,11 @@ class RespondToControllerTest < ActionController::TestCase
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
@@ -508,6 +517,13 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
end
+ def test_json_with_callback_sets_javascript_content_type
+ @request.accept = 'application/json'
+ get :json_with_callback
+ assert_equal '/**/alert(JS)', @response.body
+ assert_equal 'text/javascript', @response.content_type
+ end
+
def test_xhr
xhr :get, :js_or_html
assert_equal 'JS', @response.body
diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb
index a70592fa1b..115f3b2f41 100644
--- a/actionpack/test/controller/mime/respond_with_test.rb
+++ b/actionpack/test/controller/mime/respond_with_test.rb
@@ -2,6 +2,10 @@ require 'abstract_unit'
require 'controller/fake_models'
class RespondWithController < ActionController::Base
+ class CustomerWithJson < Customer
+ def to_json; super; end
+ end
+
respond_to :html, :json, :touch
respond_to :xml, :except => :using_resource_with_block
respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ]
@@ -38,6 +42,10 @@ class RespondWithController < ActionController::Base
respond_with(resource, :location => "http://test.host/", :status => :created)
end
+ def using_resource_with_json
+ respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13))
+ end
+
def using_invalid_resource_with_template
respond_with(resource)
end
@@ -138,8 +146,6 @@ class EmptyRespondWithController < ActionController::Base
end
class RespondWithControllerTest < ActionController::TestCase
- tests RespondWithController
-
def setup
super
@request.host = "www.example.com"
@@ -382,9 +388,8 @@ class RespondWithControllerTest < ActionController::TestCase
end
def test_using_resource_for_put_with_json_yields_no_content_on_success
- Customer.any_instance.stubs(:to_json).returns('{"name": "David"}')
@request.accept = "application/json"
- put :using_resource
+ put :using_resource_with_json
assert_equal "application/json", @response.content_type
assert_equal 204, @response.status
assert_equal "", @response.body
@@ -433,10 +438,9 @@ class RespondWithControllerTest < ActionController::TestCase
end
def test_using_resource_for_delete_with_json_yields_no_content_on_success
- Customer.any_instance.stubs(:to_json).returns('{"name": "David"}')
Customer.any_instance.stubs(:destroyed?).returns(true)
@request.accept = "application/json"
- delete :using_resource
+ delete :using_resource_with_json
assert_equal "application/json", @response.content_type
assert_equal 204, @response.status
assert_equal "", @response.body
@@ -645,6 +649,8 @@ class RespondWithControllerTest < ActionController::TestCase
get :index, format: 'csv'
assert_equal Mime::CSV, @response.content_type
assert_equal "c,s,v", @response.body
+ ensure
+ ActionController::Renderers.remove :csv
end
def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer
@@ -654,6 +660,23 @@ class RespondWithControllerTest < ActionController::TestCase
end
end
+ def test_removing_renderers
+ ActionController::Renderers.add :csv do |obj, options|
+ send_data obj.to_csv, type: Mime::CSV
+ end
+ @controller = CsvRespondWithController.new
+ @request.accept = "text/csv"
+ get :index, format: 'csv'
+ assert_equal Mime::CSV, @response.content_type
+
+ ActionController::Renderers.remove :csv
+ assert_raise ActionController::MissingRenderer do
+ get :index, format: 'csv'
+ end
+ ensure
+ ActionController::Renderers.remove :csv
+ end
+
def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
@controller = EmptyRespondWithController.new
@request.accept = "*/*"
diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
index 7396c850ad..246ba099af 100644
--- a/actionpack/test/controller/new_base/bare_metal_test.rb
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -2,6 +2,8 @@ require "abstract_unit"
module BareMetalTest
class BareController < ActionController::Metal
+ include ActionController::RackDelegation
+
def index
self.response_body = "Hello world"
end
@@ -81,8 +83,8 @@ module BareMetalTest
assert_nil headers['Content-Length']
end
- test "head :continue (101) does not return a content-type header" do
- headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second
+ test "head :switching_protocols (101) does not return a content-type header" do
+ headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second
assert_nil headers['Content-Type']
assert_nil headers['Content-Length']
end
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..f4a3db8b41
--- /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" 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 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..fe11501eeb
--- /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" 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 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_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb
index 1e2191d417..5b4885f7e0 100644
--- a/actionpack/test/controller/new_base/render_implicit_action_test.rb
+++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb
@@ -6,7 +6,7 @@ module RenderImplicitAction
"render_implicit_action/simple/hello_world.html.erb" => "Hello world!",
"render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!",
"render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented"
- )]
+ ), ActionView::FileSystemResolver.new(File.expand_path('../../../controller', __FILE__))]
def hello_world() end
end
@@ -33,10 +33,25 @@ module RenderImplicitAction
assert_status 200
end
+ test "render does not traverse the file system" do
+ assert_raises(AbstractController::ActionNotFound) do
+ action_name = %w(.. .. fixtures shared).join(File::SEPARATOR)
+ SimpleController.action(action_name).call(Rack::MockRequest.env_for("/"))
+ end
+ end
+
test "available_action? returns true for implicit actions" do
assert SimpleController.new.available_action?(:hello_world)
assert SimpleController.new.available_action?(:"hyphen-ated")
assert SimpleController.new.available_action?(:not_implemented)
end
+
+ test "available_action? does not allow File::SEPARATOR on the name" do
+ action_name = %w(evil .. .. path).join(File::SEPARATOR)
+ assert_equal false, SimpleController.new.available_action?(action_name.to_sym)
+
+ action_name = %w(evil path).join(File::SEPARATOR)
+ assert_equal false, SimpleController.new.available_action?(action_name.to_sym)
+ 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..0e36d36b50
--- /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" 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 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_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
index b7a9cf92f2..e87811776a 100644
--- a/actionpack/test/controller/new_base/render_template_test.rb
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -9,7 +9,7 @@ module RenderTemplate
"locals.html.erb" => "The secret is <%= secret %>",
"xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend",
"with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>",
- "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a html template",
+ "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in an html template",
"with_implicit_raw.text.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a text template",
"test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>",
"test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>",
@@ -114,7 +114,7 @@ module RenderTemplate
get :with_implicit_raw
- assert_body "Hello <strong>this is also raw</strong> in a html template"
+ assert_body "Hello <strong>this is also raw</strong> in an html template"
assert_status 200
get :with_implicit_raw, format: 'text'
diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb
index 2a253799f3..10bad57cd6 100644
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ b/actionpack/test/controller/new_base/render_text_test.rb
@@ -14,7 +14,7 @@ module RenderText
self.view_paths = [ActionView::FixtureResolver.new]
def index
- render :text => "hello david"
+ render text: "hello david"
end
end
@@ -26,48 +26,48 @@ 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
@@ -80,7 +80,7 @@ module RenderText
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"
@@ -90,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"
@@ -106,17 +106,17 @@ module RenderText
assert_status 404
end
- test "rendering text with nil returns an empty body padded for Safari" do
+ test "rendering text with nil returns an empty body" do
get "/render_text/with_layout/with_nil"
- assert_body " "
+ 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
+ test "Rendering text with nil and custom status code returns an empty body and the status" do
get "/render_text/with_layout/with_nil_and_status"
- assert_body " "
+ assert_body ""
assert_status 403
end
@@ -127,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/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
new file mode 100644
index 0000000000..059f310d49
--- /dev/null
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
@@ -0,0 +1,29 @@
+require 'abstract_unit'
+require 'action_controller/metal/strong_parameters'
+
+class AlwaysPermittedParametersTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ ActionController::Parameters.always_permitted_parameters = %w( controller action format )
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ ActionController::Parameters.always_permitted_parameters = %w( controller action )
+ end
+
+ test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do
+ assert_deprecated do
+ ActionController::Parameters::NEVER_UNPERMITTED_PARAMS
+ end
+ end
+
+ test "permits parameters that are whitelisted" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ format: "json"
+ })
+ permitted = params.permit book: [:pages]
+ assert permitted.permitted?
+ end
+end
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/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index 33a91d72d9..aa894ffa17 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -167,9 +167,26 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
+ # Strong params has an optimization to avoid looping every time you read
+ # a key whose value is an array and building a new object. We check that
+ # optimization here.
test 'arrays are converted at most once' do
params = ActionController::Parameters.new(foo: [{}])
- assert params[:foo].equal?(params[:foo])
+ assert_same params[:foo], params[:foo]
+ end
+
+ # Strong params has an internal cache to avoid duplicated loops in the most
+ # common usage pattern. See the docs of the method `converted_arrays`.
+ #
+ # This test checks that if we push a hash to an array (in-place modification)
+ # the cache does not get fooled, the hash is still wrapped as strong params,
+ # and not permitted.
+ test 'mutated arrays are detected' do
+ params = ActionController::Parameters.new(users: [{id: 1}])
+
+ permitted = params.permit(users: [:id])
+ permitted[:users] << {injected: 1}
+ assert_not permitted[:users].last.permitted?
end
test "fetch doesnt raise ParameterMissing exception if there is a default" do
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index 11ccb6cf3b..645ecae220 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -337,14 +337,26 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase
tests ParamswrappernewsController
def test_uses_model_attribute_names_with_irregular_inflection
- ActiveSupport::Inflector.inflections do |inflect|
- inflect.irregular 'paramswrappernews_item', 'paramswrappernews'
- end
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'paramswrappernews_item', 'paramswrappernews'
+ end
- with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' }
- assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }})
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' }
+ assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }})
+ end
end
end
+
+ private
+
+ def with_dup
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+ yield
+ ensure
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
+ end
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 4331333b98..103ca9c776 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -90,6 +90,10 @@ class RedirectController < ActionController::Base
redirect_to nil
end
+ def redirect_to_params
+ redirect_to ActionController::Parameters.new(status: 200, protocol: 'javascript', f: '%0Aeval(name)')
+ end
+
def redirect_to_with_block
redirect_to proc { "http://www.rubyonrails.org/" }
end
@@ -281,6 +285,12 @@ class RedirectTest < ActionController::TestCase
end
end
+ def test_redirect_to_params
+ assert_raise(ActionController::ActionControllerError) do
+ get :redirect_to_params
+ end
+ end
+
def test_redirect_to_with_block
get :redirect_to_with_block
assert_response :redirect
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
index de8d1cbd9b..ada978aa11 100644
--- a/actionpack/test/controller/render_json_test.rb
+++ b/actionpack/test/controller/render_json_test.rb
@@ -101,7 +101,7 @@ class RenderJsonTest < ActionController::TestCase
def test_render_json_with_callback
xhr :get, :render_json_hello_world_with_callback
- assert_equal 'alert({"hello":"world"})', @response.body
+ assert_equal '/**/alert({"hello":"world"})', @response.body
assert_equal 'text/javascript', @response.content_type
end
diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb
index b5e74e373d..af50e11261 100644
--- a/actionpack/test/controller/render_other_test.rb
+++ b/actionpack/test/controller/render_other_test.rb
@@ -1,9 +1,5 @@
require 'abstract_unit'
-ActionController.add_renderer :simon do |says, options|
- self.content_type = Mime::TEXT
- self.response_body = "Simon says: #{says}"
-end
class RenderOtherTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -15,7 +11,14 @@ class RenderOtherTest < ActionController::TestCase
tests TestController
def test_using_custom_render_option
+ ActionController.add_renderer :simon do |says, options|
+ self.content_type = Mime::TEXT
+ self.response_body = "Simon says: #{says}"
+ end
+
get :render_simon_says
assert_equal "Simon says: foo", @response.body
+ ensure
+ ActionController.remove_renderer :simon
end
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 26806fb03f..9926130c02 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -242,6 +242,8 @@ class MetalTestController < ActionController::Metal
include AbstractController::Rendering
include ActionView::Rendering
include ActionController::Rendering
+ include ActionController::RackDelegation
+
def accessing_logger_in_template
render :inline => "<%= logger.class %>"
@@ -387,10 +389,6 @@ end
class EtagRenderTest < ActionController::TestCase
tests TestControllerWithExtraEtags
- def setup
- super
- end
-
def test_multiple_etags
@request.if_none_match = etag(["123", 'ab', :cde, [:f]])
get :fresh
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 1f5fc06410..2a5aad9c0e 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -127,25 +127,26 @@ module RequestForgeryProtectionTests
@token = "cf50faa3fe97702ca1ae"
SecureRandom.stubs(:base64).returns(@token)
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
end
def teardown
- ActionController::Base.request_forgery_protection_token = nil
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
end
def test_should_render_form_with_token_tag
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
@@ -175,7 +176,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
@@ -185,21 +186,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
@@ -289,6 +290,22 @@ 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' }
@@ -360,11 +377,12 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
include RequestForgeryProtectionTests
setup do
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
end
teardown do
- ActionController::Base.request_forgery_protection_token = nil
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
end
test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
@@ -446,16 +464,38 @@ end
class CustomAuthenticityParamControllerTest < ActionController::TestCase
def setup
super
- ActionController::Base.request_forgery_protection_token = :custom_token_name
+ @old_logger = ActionController::Base.logger
+ @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ @token = "foobar"
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
+ ActionController::Base.request_forgery_protection_token = @token
end
def teardown
- ActionController::Base.request_forgery_protection_token = :authenticity_token
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
super
end
- def test_should_allow_custom_token
- post :index, :custom_token_name => 'foobar'
- assert_response :ok
+ def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
+ ActionController::Base.logger = @logger
+ SecureRandom.stubs(:base64).returns(@token)
+
+ begin
+ post :index, :custom_token_name => 'foobar'
+ assert_equal 0, @logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = @old_logger
+ end
+ end
+
+ def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token
+ ActionController::Base.logger = @logger
+
+ begin
+ post :index, :custom_token_name => 'bazqux'
+ assert_equal 1, @logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = @old_logger
+ end
end
end
diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb
index 25d0337212..6803dbbb62 100644
--- a/actionpack/test/controller/required_params_test.rb
+++ b/actionpack/test/controller/required_params_test.rb
@@ -24,10 +24,26 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase
post :create, { book: { name: "Mjallo!" } }
assert_response :ok
end
+
+ test "required parameters with false value will not raise" do
+ post :create, { book: { name: false } }
+ assert_response :ok
+ end
end
class ParametersRequireTest < ActiveSupport::TestCase
- test "required parameters must be present not merely not nil" do
+
+ test "required parameters should accept and return false value" do
+ assert_equal(false, ActionController::Parameters.new(person: false).require(:person))
+ end
+
+ test "required parameters must not be nil" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: nil).require(:person)
+ end
+ end
+
+ test "required parameters must not be empty" do
assert_raises(ActionController::ParameterMissing) do
ActionController::Parameters.new(person: {}).require(:person)
end
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index df453a0251..c18914cc8e 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -77,17 +77,18 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
include ActionDispatch::RoutingVerbs
attr_reader :rs
+ attr_accessor :controller
alias :routes :rs
def setup
- @rs = ::ActionDispatch::Routing::RouteSet.new
+ @rs = make_set
@response = nil
end
def test_symbols_with_dashes
rs.draw do
get '/:artist/:song-omg', :to => lambda { |env|
- resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
@@ -99,7 +100,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_id_with_dash
rs.draw do
get '/journey/:id', :to => lambda { |env|
- resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
@@ -111,7 +112,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_dash_with_custom_regexp
rs.draw do
get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env|
- resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
@@ -124,7 +125,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_pre_dash
rs.draw do
get '/:artist/omg-:song', :to => lambda { |env|
- resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
@@ -136,7 +137,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_pre_dash_with_custom_regexp
rs.draw do
get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env|
- resp = ActiveSupport::JSON.encode env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
@@ -243,6 +244,32 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal 'clients', get(URI('http://clients.example.org/'))
end
+ def test_scoped_lambda
+ scope_called = false
+ rs.draw do
+ scope '/foo', :constraints => lambda { |req| scope_called = true } do
+ get '/', :to => lambda { |env| [200, {}, %w{default}] }
+ end
+ end
+
+ assert_equal 'default', get(URI('http://www.example.org/foo/'))
+ assert scope_called, "scope constraint should be called"
+ end
+
+ def test_scoped_lambda_with_get_lambda
+ inner_called = false
+
+ rs.draw do
+ scope '/foo', :constraints => lambda { |req| flunk "should not be called" } do
+ get '/', :constraints => lambda { |req| inner_called = true },
+ :to => lambda { |env| [200, {}, %w{default}] }
+ end
+ end
+
+ assert_equal 'default', get(URI('http://www.example.org/foo/'))
+ assert inner_called, "inner constraint should be called"
+ end
+
def test_empty_string_match
rs.draw do
get '/:username', :constraints => { :username => /[^\/]+/ },
@@ -291,11 +318,16 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal '/admin/user/show/10', url_for(rs, { :controller => 'admin/user', :action => 'show', :id => 10 })
- assert_equal '/admin/user/show', url_for(rs, { :action => 'show' }, { :controller => 'admin/user', :action => 'list', :id => '10' })
- assert_equal '/admin/user/list/10', url_for(rs, {}, { :controller => 'admin/user', :action => 'list', :id => '10' })
+ get URI('http://test.host/admin/user/list/10')
+
+ assert_equal({ :controller => 'admin/user', :action => 'list', :id => '10' },
+ controller.request.path_parameters)
- assert_equal '/admin/stuff', url_for(rs, { :controller => 'stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' })
- assert_equal '/stuff', url_for(rs, { :controller => '/stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' })
+ assert_equal '/admin/user/show', controller.url_for({ :action => 'show', :only_path => true })
+ assert_equal '/admin/user/list/10', controller.url_for({:only_path => true})
+
+ assert_equal '/admin/stuff', controller.url_for({ :controller => 'stuff', :only_path => true })
+ assert_equal '/stuff', controller.url_for({ :controller => '/stuff', :only_path => true })
end
def test_ignores_leading_slash
@@ -419,14 +451,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get 'page' => 'content#show_page', :as => 'pages', :host => 'foo.com'
end
routes = setup_for_named_route
- routes.expects(:url_for).with({
- :host => 'foo.com',
- :only_path => false,
- :controller => 'content',
- :action => 'show_page',
- :use_route => 'pages'
- }).once
- routes.send(:pages_url)
+ assert_equal "http://foo.com/page", routes.pages_url
end
def setup_for_named_route(options = {})
@@ -499,9 +524,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_changing_controller
rs.draw { get ':controller/:action/:id' }
+ get URI('http://test.host/admin/user/index/10')
+
assert_equal '/admin/stuff/show/10',
- url_for(rs, {:controller => 'stuff', :action => 'show', :id => 10},
- {:controller => 'admin/user', :action => 'index'})
+ controller.url_for({:controller => 'stuff', :action => 'show', :id => 10, :only_path => true})
end
def test_paths_escaped
@@ -560,8 +586,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get '*path' => 'content#show_file'
end
+ get URI('http://test.host/pages/boo')
+ assert_equal({:controller=>"content", :action=>"show_file", :path=>"pages/boo"},
+ controller.request.path_parameters)
+
assert_equal '/pages/boo',
- url_for(rs, {}, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) })
+ controller.url_for(:only_path => true)
end
def test_backwards
@@ -570,7 +600,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get ':controller(/:action(/:id))'
end
- assert_equal '/page/20', url_for(rs, { :id => 20 }, { :controller => 'pages', :action => 'show' })
+ get URI('http://test.host/pages/show')
+ assert_equal '/page/20', controller.url_for({ :id => 20, :only_path => true })
assert_equal '/page/20', url_for(rs, { :controller => 'pages', :id => 20, :action => 'show' })
assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' })
end
@@ -611,7 +642,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_action_expiry
rs.draw { get ':controller(/:action(/:id))' }
- assert_equal '/content', url_for(rs, { :controller => 'content' }, { :controller => 'content', :action => 'show' })
+ get URI('http://test.host/content/show')
+ assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true)
end
def test_requirement_should_prevent_optional_id
@@ -654,14 +686,18 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal '/pages/2005/6/12',
url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12 })
+ get URI('http://test.host/pages/2005/6/12')
+ assert_equal({ :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' },
+ controller.request.path_parameters)
+
assert_equal '/pages/2005/6/4',
- url_for(rs, { :day => 4 }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' })
+ controller.url_for({ :day => 4, :only_path => true })
assert_equal '/pages/2005/6',
- url_for(rs, { :day => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' })
+ controller.url_for({ :day => nil, :only_path => true })
assert_equal '/pages/2005',
- url_for(rs, { :day => nil, :month => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' })
+ controller.url_for({ :day => nil, :month => nil, :only_path => true })
end
def test_root_url_generation_with_controller_and_action
@@ -819,9 +855,15 @@ end
class RouteSetTest < ActiveSupport::TestCase
include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
- def set
- @set ||= ROUTING::RouteSet.new
+ attr_reader :set
+ alias :routes :set
+ attr_accessor :controller
+
+ def setup
+ super
+ @set = make_set
end
def request
@@ -922,7 +964,8 @@ class RouteSetTest < ActiveSupport::TestCase
get '/admin/users' => 'admin/users#index', :as => "users"
end
- MockController.build(set.url_helpers).new
+ get URI('http://test.host/people')
+ controller
end
def test_named_route_url_method
@@ -1019,12 +1062,12 @@ class RouteSetTest < ActiveSupport::TestCase
get '/:controller(/:action(/:id))'
end
- assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages'))
- assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index'))
- assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list'))
+ assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages'))
+ assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages/index'))
+ assert_equal({:controller => 'pages', :action => 'list'}, request_path_params('/pages/list'))
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10'))
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/pages/show/10'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10'))
end
def test_route_constraints_on_request_object_with_anchors_are_valid
@@ -1076,9 +1119,7 @@ class RouteSetTest < ActiveSupport::TestCase
get "/people" => "missing#index"
end
- assert_raise(ActionController::RoutingError) {
- set.recognize_path("/people", :method => :get)
- }
+ assert_raises(ActionController::RoutingError) { request_path_params '/people' }
end
def test_recognize_with_encoded_id_and_regex
@@ -1086,8 +1127,8 @@ class RouteSetTest < ActiveSupport::TestCase
get 'page/:id' => 'pages#show', :id => /[a-zA-Z0-9\+]+/
end
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10'))
- assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, set.recognize_path('/page/hello+world'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, request_path_params('/page/hello+world'))
end
def test_recognize_with_http_methods
@@ -1100,40 +1141,40 @@ class RouteSetTest < ActiveSupport::TestCase
delete "/people/:id" => "people#destroy"
end
- params = set.recognize_path("/people", :method => :get)
+ params = request_path_params("/people", :method => :get)
assert_equal("index", params[:action])
- params = set.recognize_path("/people", :method => :post)
+ params = request_path_params("/people", :method => :post)
assert_equal("create", params[:action])
- params = set.recognize_path("/people/5", :method => :put)
+ params = request_path_params("/people/5", :method => :put)
assert_equal("update", params[:action])
- params = set.recognize_path("/people/5", :method => :patch)
+ params = request_path_params("/people/5", :method => :patch)
assert_equal("update", params[:action])
assert_raise(ActionController::UnknownHttpMethod) {
- set.recognize_path("/people", :method => :bacon)
+ request_path_params("/people", :method => :bacon)
}
- params = set.recognize_path("/people/5", :method => :get)
+ params = request_path_params("/people/5", :method => :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :put)
+ params = request_path_params("/people/5", :method => :put)
assert_equal("update", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :patch)
+ params = request_path_params("/people/5", :method => :patch)
assert_equal("update", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :delete)
+ params = request_path_params("/people/5", :method => :delete)
assert_equal("destroy", params[:action])
assert_equal("5", params[:id])
assert_raise(ActionController::RoutingError) {
- set.recognize_path("/people/5", :method => :post)
+ request_path_params("/people/5", :method => :post)
}
end
@@ -1143,11 +1184,11 @@ class RouteSetTest < ActiveSupport::TestCase
root :to => "people#index"
end
- params = set.recognize_path("/people", :method => :get)
+ params = request_path_params("/people", :method => :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
- params = set.recognize_path("/", :method => :get)
+ params = request_path_params("/", :method => :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
end
@@ -1158,7 +1199,7 @@ class RouteSetTest < ActiveSupport::TestCase
:year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/
end
- params = set.recognize_path("/articles/2005/11/05/a-very-interesting-article", :method => :get)
+ params = request_path_params("/articles/2005/11/05/a-very-interesting-article", :method => :get)
assert_equal("permalink", params[:action])
assert_equal("2005", params[:year])
assert_equal("11", params[:month])
@@ -1172,7 +1213,7 @@ class RouteSetTest < ActiveSupport::TestCase
get '/profile' => 'profile#index'
end
- set.recognize_path("/profile") rescue nil
+ request_path_params("/profile") rescue nil
assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded"
end
@@ -1185,17 +1226,17 @@ class RouteSetTest < ActiveSupport::TestCase
get "people/:id(.:format)" => "people#show"
end
- params = set.recognize_path("/people/5", :method => :get)
+ params = request_path_params("/people/5", :method => :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :put)
+ params = request_path_params("/people/5", :method => :put)
assert_equal("update", params[:action])
- params = set.recognize_path("/people/5", :method => :patch)
+ params = request_path_params("/people/5", :method => :patch)
assert_equal("update", params[:action])
- params = set.recognize_path("/people/5.png", :method => :get)
+ params = request_path_params("/people/5.png", :method => :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
assert_equal("png", params[:format])
@@ -1214,7 +1255,7 @@ class RouteSetTest < ActiveSupport::TestCase
def test_root_map
set.draw { root :to => 'people#index' }
- params = set.recognize_path("", :method => :get)
+ params = request_path_params("", :method => :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
end
@@ -1228,7 +1269,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
- params = set.recognize_path("/api/inventory", :method => :get)
+ params = request_path_params("/api/inventory", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
@@ -1240,7 +1281,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
end
- params = set.recognize_path("/api", :method => :get)
+ params = request_path_params("/api", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("index", params[:action])
end
@@ -1252,7 +1293,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
end
- params = set.recognize_path("/prefix/inventory", :method => :get)
+ params = request_path_params("/prefix/inventory", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
@@ -1264,38 +1305,36 @@ class RouteSetTest < ActiveSupport::TestCase
end
end
- params = set.recognize_path("/inventory", :method => :get)
+ params = request_path_params("/inventory", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
- def test_generate_changes_controller_module
- set.draw { get ':controller/:action/:id' }
- current = { :controller => "bling/bloop", :action => "bap", :id => 9 }
-
- assert_equal "/foo/bar/baz/7",
- url_for(set, { :controller => "foo/bar", :action => "baz", :id => 7 }, current)
- end
-
def test_id_is_sticky_when_it_ought_to_be
+ @set = make_set false
+
set.draw do
get ':controller/:id/:action'
end
- url = url_for(set, { :action => "destroy" }, { :controller => "people", :action => "show", :id => "7" })
- assert_equal "/people/7/destroy", url
+ get URI('http://test.host/people/7/show')
+
+ assert_equal "/people/7/destroy", controller.url_for(:action => 'destroy', :only_path => true)
end
def test_use_static_path_when_possible
+ @set = make_set false
+
set.draw do
get 'about' => "welcome#about"
get ':controller/:action/:id'
end
- url = url_for(set, { :controller => "welcome", :action => "about" },
- { :controller => "welcome", :action => "get", :id => "7" })
+ get URI('http://test.host/welcom/get/7')
- assert_equal "/about", url
+ assert_equal "/about", controller.url_for(:controller => 'welcome',
+ :action => 'about',
+ :only_path => true)
end
def test_generate
@@ -1330,38 +1369,51 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_named_routes_are_never_relative_to_modules
+ @set = make_set false
+
set.draw do
get "/connection/manage(/:action)" => 'connection/manage#index'
get "/connection/connection" => "connection/connection#index"
get '/connection' => 'connection#index', :as => 'family_connection'
end
- url = url_for(set, { :controller => "connection" }, { :controller => 'connection/manage' })
+ assert_equal({ :controller => 'connection/manage',
+ :action => 'index', }, request_path_params('/connection/manage'))
+
+ url = controller.url_for({ :controller => "connection", :only_path => true })
assert_equal "/connection/connection", url
- url = url_for(set, { :use_route => :family_connection, :controller => "connection" }, { :controller => 'connection/manage' })
+ url = controller.url_for({ :use_route => :family_connection,
+ :controller => "connection", :only_path => true })
assert_equal "/connection", url
end
def test_action_left_off_when_id_is_recalled
+ @set = make_set false
+
set.draw do
get ':controller(/:action(/:id))'
end
- assert_equal '/books', url_for(set,
- {:controller => 'books', :action => 'index'},
- {:controller => 'books', :action => 'show', :id => '10'}
- )
+
+ get URI('http://test.host/books/show/10')
+
+ assert_equal '/books', controller.url_for(:controller => 'books',
+ :only_path => true,
+ :action => 'index')
end
def test_query_params_will_be_shown_when_recalled
+ @set = make_set false
+
set.draw do
get 'show_weblog/:parameter' => 'weblog#show'
get ':controller(/:action(/:id))'
end
- assert_equal '/weblog/edit?parameter=1', url_for(set,
- {:action => 'edit', :parameter => 1},
- {:controller => 'weblog', :action => 'show', :parameter => 1}
- )
+
+ get URI('http://test.host/weblog/show/1')
+
+ assert_equal '/weblog/edit?parameter=1', controller.url_for(
+ {:action => 'edit', :parameter => 1, :only_path => true})
end
def test_format_is_not_inherit
@@ -1369,22 +1421,30 @@ class RouteSetTest < ActiveSupport::TestCase
get '/posts(.:format)' => 'posts#index'
end
- assert_equal '/posts', url_for(set,
- {:controller => 'posts'},
- {:controller => 'posts', :action => 'index', :format => 'xml'}
- )
+ get URI('http://test.host/posts.xml')
+ assert_equal({:controller => 'posts', :action => 'index', :format => 'xml'},
+ controller.request.path_parameters)
- assert_equal '/posts.xml', url_for(set,
- {:controller => 'posts', :format => 'xml'},
- {:controller => 'posts', :action => 'index', :format => 'xml'}
- )
+ assert_equal '/posts', controller.url_for(
+ {:controller => 'posts', :only_path => true})
+
+ assert_equal '/posts.xml', controller.url_for(
+ {:controller => 'posts', :format => 'xml', :only_path => true})
end
def test_expiry_determination_should_consider_values_with_to_param
+ @set = make_set false
+
set.draw { get 'projects/:project_id/:controller/:action' }
- assert_equal '/projects/1/weblog/show', url_for(set,
- { :action => 'show', :project_id => 1 },
- { :controller => 'weblog', :action => 'show', :project_id => '1' })
+
+ get URI('http://test.host/projects/1/weblog/show')
+
+ assert_equal(
+ { :controller => 'weblog', :action => 'show', :project_id => '1' },
+ controller.request.path_parameters)
+
+ assert_equal '/projects/1/weblog/show',
+ controller.url_for({ :action => 'show', :project_id => 1, :only_path => true })
end
def test_named_route_in_nested_resource
@@ -1587,7 +1647,6 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_slashes_are_implied
- @set = nil
set.draw { get("/:controller(/:action(/:id))") }
assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' })
@@ -1685,7 +1744,43 @@ class RouteSetTest < ActiveSupport::TestCase
assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 })
end
+ include ActionDispatch::RoutingVerbs
+
+ class TestSet < ROUTING::RouteSet
+ def initialize(block)
+ @block = block
+ super()
+ end
+
+ class Dispatcher < ROUTING::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
+ end
+
+ alias :routes :set
+
def test_generate_with_optional_params_recalls_last_request
+ controller = nil
+ @set = TestSet.new ->(c) { controller = c }
+
set.draw do
get "blog/", :controller => "blog", :action => "index"
@@ -1700,23 +1795,29 @@ class RouteSetTest < ActiveSupport::TestCase
get "*anything", :controller => "blog", :action => "unknown_request"
end
- assert_equal({:controller => "blog", :action => "index"}, set.recognize_path("/blog"))
- assert_equal({:controller => "blog", :action => "show", :id => "123"}, set.recognize_path("/blog/show/123"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, set.recognize_path("/blog/2004"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, set.recognize_path("/blog/2004/12"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, set.recognize_path("/blog/2004/12/25"))
- assert_equal({:controller => "articles", :action => "edit", :id => "123"}, set.recognize_path("/blog/articles/edit/123"))
- assert_equal({:controller => "articles", :action => "show_stats"}, set.recognize_path("/blog/articles/show_stats"))
- assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, set.recognize_path("/blog/wibble"))
- assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, set.recognize_path("/junk"))
+ recognize_path = ->(path) {
+ get(URI("http://example.org" + path))
+ controller.request.path_parameters
+ }
- last_request = set.recognize_path("/blog/2006/07/28").freeze
- assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, last_request)
- assert_equal("/blog/2006/07/25", url_for(set, { :day => 25 }, last_request))
- assert_equal("/blog/2005", url_for(set, { :year => 2005 }, last_request))
- assert_equal("/blog/show/123", url_for(set, { :action => "show" , :id => 123 }, last_request))
- assert_equal("/blog/2006", url_for(set, { :year => 2006 }, last_request))
- assert_equal("/blog/2006", url_for(set, { :year => 2006, :month => nil }, last_request))
+ assert_equal({:controller => "blog", :action => "index"}, recognize_path.("/blog"))
+ assert_equal({:controller => "blog", :action => "show", :id => "123"}, recognize_path.("/blog/show/123"))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, recognize_path.("/blog/2004"))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, recognize_path.("/blog/2004/12"))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, recognize_path.("/blog/2004/12/25"))
+ assert_equal({:controller => "articles", :action => "edit", :id => "123"}, recognize_path.("/blog/articles/edit/123"))
+ assert_equal({:controller => "articles", :action => "show_stats"}, recognize_path.("/blog/articles/show_stats"))
+ assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, recognize_path.("/blog/wibble"))
+ assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, recognize_path.("/junk"))
+
+ get URI('http://example.org/blog/2006/07/28')
+
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, controller.request.path_parameters)
+ assert_equal("/blog/2006/07/25", controller.url_for({ :day => 25, :only_path => true }))
+ assert_equal("/blog/2005", controller.url_for({ :year => 2005, :only_path => true }))
+ assert_equal("/blog/show/123", controller.url_for({ :action => "show" , :id => 123, :only_path => true }))
+ assert_equal("/blog/2006", controller.url_for({ :year => 2006, :only_path => true }))
+ assert_equal("/blog/2006", controller.url_for({ :year => 2006, :month => nil, :only_path => true }))
end
private
@@ -1789,6 +1890,9 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
root :to => "news#index"
}
+ attr_reader :routes
+ attr_reader :controller
+
def setup
@routes = ActionDispatch::Routing::RouteSet.new
@routes.draw(&Mapping)
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index 4df2f8b98d..c002cf4d8f 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -9,6 +9,7 @@ end
class SendFileController < ActionController::Base
include TestFileUtils
+ include ActionController::Testing
layout "layouts/standard" # to make sure layouts don't interfere
attr_writer :options
@@ -30,11 +31,8 @@ class SendFileWithActionControllerLive < SendFileController
end
class SendFileTest < ActionController::TestCase
- tests SendFileController
include TestFileUtils
- Mime::Type.register "image/png", :png unless defined? Mime::PNG
-
def setup
@controller = SendFileController.new
@request = ActionController::TestRequest.new
@@ -201,8 +199,6 @@ class SendFileTest < ActionController::TestCase
end
end
- tests SendFileWithActionControllerLive
-
def test_send_file_with_action_controller_live
@controller = SendFileWithActionControllerLive.new
@controller.options = { :content_type => "application/x-ruby" }
diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb
index ff23b22040..f7eba1ef43 100644
--- a/actionpack/test/controller/show_exceptions_test.rb
+++ b/actionpack/test/controller/show_exceptions_test.rb
@@ -32,7 +32,7 @@ module ShowExceptions
test 'show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?' do
@app = ShowExceptionsController.action(:boom)
- ['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
+ ['127.0.0.1', '127.0.0.127', '127.12.1.1', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
self.remote_addr = ip_address
get '/'
assert_match(/boom/, body)
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 5ff4a383ec..060c940100 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -163,6 +163,29 @@ XML
end
end
+ class DefaultUrlOptionsCachingController < ActionController::Base
+ before_action { @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
@@ -198,7 +221,7 @@ XML
assert_equal 200, @response.status
end
- def test_head_params_as_sting
+ def test_head_params_as_string
assert_raise(NoMethodError) { head :test_params, "document body", :id => 10 }
end
@@ -639,7 +662,7 @@ XML
def test_id_converted_to_string
get :test_params, :id => 20, :foo => Object.new
- assert_kind_of String, @request.path_parameters['id']
+ assert_kind_of String, @request.path_parameters[:id]
end
def test_array_path_parameter_handled_properly
@@ -650,17 +673,17 @@ XML
end
get :test_params, :path => ['hello', 'world']
- assert_equal ['hello', 'world'], @request.path_parameters['path']
- assert_equal 'hello/world', @request.path_parameters['path'].to_param
+ assert_equal ['hello', 'world'], @request.path_parameters[:path]
+ assert_equal 'hello/world', @request.path_parameters[:path].to_param
end
end
def test_assert_realistic_path_parameters
get :test_params, :id => 20, :foo => Object.new
- # All elements of path_parameters should use string keys
+ # All elements of path_parameters should use Symbol keys
@request.path_parameters.keys.each do |key|
- assert_kind_of String, key
+ assert_kind_of Symbol, key
end
end
@@ -690,6 +713,7 @@ XML
def test_header_properly_reset_after_remote_http_request
xhr :get, :test_params
assert_nil @request.env['HTTP_X_REQUESTED_WITH']
+ assert_nil @request.env['HTTP_ACCEPT']
end
def test_header_properly_reset_after_get_request
@@ -698,10 +722,10 @@ XML
assert_nil @request.instance_variable_get("@request_method")
end
- def test_params_reset_after_post_request
+ def test_params_reset_between_post_requests
post :no_op, :foo => "bar"
assert_equal "bar", @request.params[:foo]
- @request.recycle!
+
post :no_op
assert @request.params[:foo].blank?
end
@@ -714,12 +738,12 @@ XML
assert_equal "baz", @request.filtered_parameters[:foo]
end
- def test_symbolized_path_params_reset_after_request
+ def test_path_params_reset_between_request
get :test_params, :id => "foo"
- assert_equal "foo", @request.symbolized_path_parameters[:id]
- @request.recycle!
+ assert_equal "foo", @request.path_parameters[:id]
+
get :test_params
- assert_nil @request.symbolized_path_parameters[:id]
+ assert_nil @request.path_parameters[:id]
end
def test_request_protocol_is_reset_after_request
diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb
index 6c2311e7a5..24a09222b1 100644
--- a/actionpack/test/controller/url_for_integration_test.rb
+++ b/actionpack/test/controller/url_for_integration_test.rb
@@ -6,6 +6,7 @@ require 'active_support/core_ext/object/with_options'
module ActionPack
class URLForIntegrationTest < ActiveSupport::TestCase
include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
Model = Struct.new(:to_param)
@@ -61,8 +62,11 @@ module ActionPack
root :to => "news#index"
}
+ attr_reader :routes
+ attr_accessor :controller
+
def setup
- @routes = ActionDispatch::Routing::RouteSet.new
+ @routes = make_set false
@routes.draw(&Mapping)
end
@@ -70,9 +74,9 @@ module ActionPack
['/admin/users',[ { :use_route => 'admin_users' }]],
['/admin/users',[ { :controller => 'admin/users' }]],
['/admin/users',[ { :controller => 'admin/users', :action => 'index' }]],
- ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users' }]],
- ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }]],
- ['/people',[ { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }]],
+ ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users', :action => 'index' }, '/admin/users']],
+ ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts', :action => 'show', :id => '1' }, '/admin/accounts/show/1']],
+ ['/people',[ { :controller => '/people', :action => 'index' }, {:controller=>"admin/accounts", :action=>"foo", :id=>"bar"}, '/admin/accounts/foo/bar']],
['/admin/posts',[ { :controller => 'admin/posts' }]],
['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]],
@@ -86,11 +90,11 @@ module ActionPack
['/archive?year=january',[ { :controller => 'archive', :action => 'index', :year => 'january' }]],
['/people',[ { :controller => 'people', :action => 'index' }]],
- ['/people',[ { :action => 'index' }, { :controller => 'people' }]],
- ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people',[ {}, { :controller => 'people', :action => 'index' }]],
- ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }]],
+ ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'index' }, '/people']],
+ ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people',[ {}, { :controller => 'people', :action => 'index' }, '/people']],
+ ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
['/people/new',[ { :use_route => 'new_person' }]],
['/people/new',[ { :controller => 'people', :action => 'new' }]],
['/people/1',[ { :use_route => 'person', :id => '1' }]],
@@ -98,11 +102,11 @@ module ActionPack
['/people/1.xml',[ { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => 1 }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => Model.new('1') }]],
- ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }]],
- ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }]],
+ ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }, '/people']],
+ ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }, '/people/index/1']],
['/people/1/edit',[ { :controller => 'people', :action => 'edit', :id => '1' }]],
['/people/1/edit.xml',[ { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }]],
['/people/1/edit',[ { :use_route => 'edit_person', :id => '1' }]],
@@ -118,16 +122,15 @@ module ActionPack
['/project',[ { :controller => 'project', :action => 'index' }]],
['/projects/1',[ { :controller => 'project', :action => 'index', :project_id => '1' }]],
- ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1' }]],
+ ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']],
['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]],
- ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }]],
+ ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :controller => 'project', :action => 'index', :project_id => '1' }, '/projects/1']],
['/clients',[ { :controller => 'projects', :action => 'index' }]],
['/clients?project_id=1',[ { :controller => 'projects', :action => 'index', :project_id => '1' }]],
- ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1' }]],
- ['/clients',[ { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }]],
+ ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']],
- ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }]],
+ ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }, '/comments/show']],
['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]],
['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]],
@@ -144,24 +147,21 @@ module ActionPack
['/notes',[ { :page_id => nil, :controller => 'notes' }]],
['/notes',[ { :controller => 'notes' }]],
['/notes/print',[ { :controller => 'notes', :action => 'print' }]],
- ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }]],
-
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]],
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }]],
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]],
- ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1' }]],
- ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1' }]],
- ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }]],
- ['/notes/index/1',[ { :controller => 'notes', :id => '1' }, { :foo => 'bar' }]],
- ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }]],
- ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }]],
+ ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }, '/notes/print']],
+
+ ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :action => 'index', :id => '1' }, '/notes/index/1']],
+ ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
+ ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
+ ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
+ ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
+ ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
+ ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]],
['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]],
['/posts',[ { :controller => 'posts' }]],
['/posts',[ { :controller => 'posts', :action => 'index' }]],
- ['/posts',[ { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }]],
- ['/posts/create',[ { :action => 'create' }, { :controller => 'posts' }]],
+ ['/posts/create',[ { :action => 'create' }, {:day=>nil, :month=>nil, :controller=>"posts", :action=>"show_date"}, '/blog']],
['/posts?foo=bar',[ { :controller => 'posts', :foo => 'bar' }]],
['/posts?foo%5B%5D=bar&foo%5B%5D=baz', [{ :controller => 'posts', :foo => ['bar', 'baz'] }]],
['/posts?page=2', [{ :controller => 'posts', :page => 2 }]],
@@ -169,9 +169,20 @@ module ActionPack
['/news.rss', [{ :controller => 'news', :action => 'index', :format => 'rss' }]],
].each_with_index do |(url, params), i|
- define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
- assert_equal url, url_for(@routes, *params), params.inspect
- end
+ if params.length > 1
+ hash, path_params, route = *params
+ hash[:only_path] = true
+
+ define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
+ get URI('http://test.host' + route.to_s)
+ assert_equal path_params, controller.request.path_parameters
+ assert_equal url, controller.url_for(hash), params.inspect
+ end
+ else
+ define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
+ assert_equal url, url_for(@routes, params.first), params.inspect
+ end
+ end
end
end
end
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index a8035e5bd7..9f086af664 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -11,6 +11,26 @@ module AbstractController
W.default_url_options.clear
end
+ def test_nested_optional
+ klass = Class.new {
+ include ActionDispatch::Routing::RouteSet.new.tap { |r|
+ r.draw {
+ get "/foo/(:bar/(:baz))/:zot", :as => 'fun',
+ :controller => :articles,
+ :action => :index
+ }
+ }.url_helpers
+ self.default_url_options[:host] = 'example.com'
+ }
+
+ path = klass.new.fun_path({:controller => :articles,
+ :baz => "baz",
+ :zot => "zot",
+ :only_path => true })
+ # :bar key isn't provided
+ assert_equal '/foo/zot', path
+ end
+
def add_host!
W.default_url_options[:host] = 'www.basecamphq.com'
end
@@ -75,7 +95,7 @@ module AbstractController
end
def test_subdomain_may_be_object
- model = mock(:to_param => 'api')
+ model = Class.new { def self.to_param; 'api'; end }
add_host!
assert_equal('http://api.basecamphq.com/c/a/i',
W.new.url_for(:subdomain => model, :controller => 'c', :action => 'a', :id => 'i')
@@ -169,6 +189,18 @@ module AbstractController
)
end
+ def test_without_protocol_and_with_port
+ add_host!
+ add_port!
+
+ assert_equal('//www.basecamphq.com:3000/c/a/i',
+ W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => '//')
+ )
+ assert_equal('//www.basecamphq.com:3000/c/a/i',
+ W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => false)
+ )
+ end
+
def test_trailing_slash
add_host!
options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'}
@@ -255,12 +287,12 @@ module AbstractController
# We need to create a new class in order to install the new named route.
kls = Class.new { include set.url_helpers }
controller = kls.new
- assert controller.respond_to?(:home_url)
+ assert_respond_to controller, :home_url
assert_equal '/brave/new/world',
- controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true)
+ controller.url_for(:controller => 'brave', :action => 'new', :id => 'world', :only_path => true)
- assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true))
- assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama'))
+ assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused', :only_path => true))
+ assert_equal("/home/sweet/home/alabama", controller.home_path('alabama'))
end
end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index ba7aaa338d..0f145666d1 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -681,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"
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index 3045a07ad6..24526fb00e 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -10,6 +10,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@closed = false
end
+ # We're obliged to implement this (even though it doesn't actually
+ # get called here) to properly comply with the Rack SPEC
def each
end
@@ -43,6 +45,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
@@ -134,9 +149,10 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
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(/puke/, body)
+ assert_match(/RuntimeError\npuke/, body)
get "/not_found", {}, xhr_request_env
assert_response 404
@@ -242,4 +258,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/header_test.rb b/actionpack/test/dispatch/header_test.rb
index 9e37b96951..e2b38c23bc 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -55,6 +55,8 @@ class HeaderTest < ActiveSupport::TestCase
test "key?" do
assert @headers.key?("CONTENT_TYPE")
assert @headers.include?("CONTENT_TYPE")
+ assert @headers.key?("Content-Type")
+ assert @headers.include?("Content-Type")
end
test "fetch with block" do
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/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb
index 58457b0c28..889f9a4736 100644
--- a/actionpack/test/dispatch/mapper_test.rb
+++ b/actionpack/test/dispatch/mapper_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
module ActionDispatch
module Routing
class MapperTest < ActiveSupport::TestCase
- class FakeSet
+ class FakeSet < ActionDispatch::Routing::RouteSet
attr_reader :routes
alias :set :routes
@@ -38,7 +38,7 @@ module ActionDispatch
def test_mapping_requirements
options = { :controller => 'foo', :action => 'bar', :via => :get }
- m = Mapper::Mapping.new FakeSet.new, {}, '/store/:name(*rest)', options
+ m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options)
_, _, requirements, _ = m.to_route
assert_equal(/.+?/, requirements[:rest])
end
@@ -72,7 +72,7 @@ module ActionDispatch
mapper = Mapper.new fakeset
mapper.get '/*path/foo/:bar', :to => 'pages#show'
assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info]
- assert_nil fakeset.requirements.first[:path]
+ assert_equal(/.+?/, fakeset.requirements.first[:path])
end
def test_map_wildcard_with_multiple_wildcard
@@ -80,7 +80,7 @@ module ActionDispatch
mapper = Mapper.new fakeset
mapper.get '/*foo/*bar', :to => 'pages#show'
assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info]
- assert_nil fakeset.requirements.first[:foo]
+ assert_equal(/.+?/, fakeset.requirements.first[:foo])
assert_equal(/.+?/, fakeset.requirements.first[:bar])
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 981cf2426e..d29cc8473e 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -1,8 +1,6 @@
require 'abstract_unit'
class MimeTypeTest < ActiveSupport::TestCase
- Mime::Type.register "image/png", :png unless defined? Mime::PNG
- Mime::Type.register "application/pdf", :pdf unless defined? Mime::PDF
test "parse single" do
Mime::LOOKUP.keys.each do |mime_type|
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index cdf00d84fb..d5a4d8ee11 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -1,12 +1,18 @@
require 'abstract_unit'
+require 'rails/engine'
class TestRoutingMount < ActionDispatch::IntegrationTest
Router = ActionDispatch::Routing::RouteSet.new
- class FakeEngine
+ class AppWithRoutes < Rails::Engine
def self.routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
+ end
+
+ # Test for mounting apps that respond to routes, but aren't Rails-like apps.
+ class SinatraLikeApp
+ def self.routes; Object.new; end
def self.call(env)
[200, {"Content-Type" => "text/html"}, ["OK"]]
@@ -21,20 +27,23 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
mount SprocketsApp, :at => "/sprockets"
mount SprocketsApp => "/shorthand"
- mount FakeEngine, :at => "/fakeengine", :as => :fake
- mount FakeEngine, :at => "/getfake", :via => :get
+ mount SinatraLikeApp, :at => "/fakeengine", :as => :fake
+ mount SinatraLikeApp, :at => "/getfake", :via => :get
scope "/its_a" do
mount SprocketsApp, :at => "/sprocket"
end
resources :users do
- mount FakeEngine, :at => "/fakeengine", :as => :fake_mounted_at_resource
+ mount AppWithRoutes, :at => "/fakeengine", :as => :fake_mounted_at_resource
end
+
+ mount SprocketsApp, :at => "/", :via => :get
end
+ APP = RoutedRackApp.new Router
def app
- Router
+ APP
end
def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources
@@ -44,6 +53,11 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
"A named route should be defined with a parent's prefix"
end
+ def test_mounting_at_root_path
+ get "/omg"
+ assert_equal " -- /omg", response.body
+ end
+
def test_mounting_sets_script_name
get "/sprockets/omg"
assert_equal "/sprockets -- /omg", response.body
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index 08501d19c0..f90d5499d7 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'rack/test'
+require 'rails/engine'
module TestGenerationPrefix
class Post
@@ -15,78 +16,56 @@ module TestGenerationPrefix
ActiveModel::Name.new(klass)
end
+
+ def to_model; self; end
+ def persisted?; true; end
end
class WithMountedEngine < ActionDispatch::IntegrationTest
include Rack::Test::Methods
- class BlogEngine
- def self.routes
- @routes ||= begin
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- get "/posts/:id", :to => "inside_engine_generating#show", :as => :post
- get "/posts", :to => "inside_engine_generating#index", :as => :posts
- get "/url_to_application", :to => "inside_engine_generating#url_to_application"
- 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
- end
- end
-
- def self.call(env)
- env['action_dispatch.routes'] = routes
- routes.call(env)
+ class BlogEngine < Rails::Engine
+ routes.draw do
+ get "/posts/:id", :to => "inside_engine_generating#show", :as => :post
+ get "/posts", :to => "inside_engine_generating#index", :as => :posts
+ get "/url_to_application", :to => "inside_engine_generating#url_to_application"
+ 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
end
- class RailsApplication
- def self.routes
- @routes ||= begin
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- scope "/:omg", :omg => "awesome" do
- mount BlogEngine => "/blog", :as => "blog_engine"
- end
- get "/posts/:id", :to => "outside_engine_generating#post", :as => :post
- get "/generate", :to => "outside_engine_generating#index"
- get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app"
- get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine"
- get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for"
- get "/conflicting_url", :to => "outside_engine_generating#conflicting"
- get "/ivar_usage", :to => "outside_engine_generating#ivar_usage"
- root :to => "outside_engine_generating#index"
- end
-
- routes
+ class RailsApplication < Rails::Engine
+ routes.draw do
+ scope "/:omg", :omg => "awesome" do
+ mount BlogEngine => "/blog", :as => "blog_engine"
end
- end
-
- def self.call(env)
- env['action_dispatch.routes'] = routes
- routes.call(env)
+ get "/posts/:id", :to => "outside_engine_generating#post", :as => :post
+ get "/generate", :to => "outside_engine_generating#index"
+ get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app"
+ get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine"
+ get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for"
+ get "/conflicting_url", :to => "outside_engine_generating#conflicting"
+ get "/ivar_usage", :to => "outside_engine_generating#ivar_usage"
+ root :to => "outside_engine_generating#index"
end
end
# force draw
- RailsApplication.routes
RailsApplication.routes.define_mounted_helper(:main_app)
class ::InsideEngineGeneratingController < ActionController::Base
@@ -158,19 +137,15 @@ module TestGenerationPrefix
end
def app
- RailsApplication
+ RailsApplication.instance
end
- def engine_object
- @engine_object ||= EngineObject.new
- end
-
- def app_object
- @app_object ||= AppObject.new
- end
+ attr_reader :engine_object, :app_object
def setup
RailsApplication.routes.default_url_options = {}
+ @engine_object = EngineObject.new
+ @app_object = AppObject.new
end
include BlogEngine.routes.mounted_helpers
@@ -392,27 +367,12 @@ module TestGenerationPrefix
end
end
- class RailsApplication
- def self.routes
- @routes ||= begin
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- mount BlogEngine => "/"
- end
-
- routes
- end
- end
-
- def self.call(env)
- env['action_dispatch.routes'] = routes
- routes.call(env)
+ class RailsApplication < Rails::Engine
+ routes.draw do
+ mount BlogEngine => "/"
end
end
- # force draw
- RailsApplication.routes
-
class ::PostsController < ActionController::Base
include BlogEngine.routes.url_helpers
include RailsApplication.routes.mounted_helpers
@@ -423,7 +383,7 @@ module TestGenerationPrefix
end
def app
- RailsApplication
+ RailsApplication.instance
end
test "generating path inside engine" do
diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb
deleted file mode 100644
index ef1964fd19..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/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb
index ce9ccfcee8..62e8197e20 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -3,6 +3,11 @@ require 'abstract_unit'
class ReloaderTest < ActiveSupport::TestCase
Reloader = ActionDispatch::Reloader
+ teardown do
+ Reloader.reset_callbacks :prepare
+ Reloader.reset_callbacks :cleanup
+ end
+
def test_prepare_callbacks
a = b = c = nil
Reloader.to_prepare { |*args| a = b = c = 1 }
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index 2a2f92b5b3..926472163e 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -8,7 +8,11 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
end
def parse
- self.class.last_request_parameters = request.request_parameters
+ self.class.last_request_parameters = begin
+ request.request_parameters
+ rescue EOFError
+ {}
+ end
self.class.last_parameters = request.parameters
head :ok
end
@@ -145,7 +149,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
test "does not raise EOFError on GET request with multipart content-type" do
with_routing do |set|
set.draw do
- get ':action', to: 'multipart_params_parsing_test/test'
+ get ':action', controller: 'multipart_params_parsing_test/test'
end
headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" }
get "/parse", {}, headers
@@ -174,7 +178,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :to => 'multipart_params_parsing_test/test'
+ post ':action', :controller => 'multipart_params_parsing_test/test'
end
yield
end
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index d82493140f..4e99c26e03 100644
--- a/actionpack/test/dispatch/request/query_string_parsing_test.rb
+++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb
@@ -105,6 +105,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
end
test "perform_deep_munge" do
+ old_perform_deep_munge = ActionDispatch::Request::Utils.perform_deep_munge
ActionDispatch::Request::Utils.perform_deep_munge = false
begin
assert_parses({"action" => nil}, "action")
@@ -115,7 +116,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
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
+ ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge
end
end
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index df55fcc8bc..10fb04e230 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_test.rb
@@ -36,29 +36,55 @@ 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
@@ -82,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 9a77454f30..1de05cbf09 100644
--- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
@@ -130,10 +130,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
end
test "ambiguous params returns a bad request" do
- with_routing do |set|
- set.draw do
- post ':action', to: ::UrlEncodedParamsParsingTest::TestController
- end
+ with_test_routing do
post "/parse", "foo[]=bar&foo[4]=bar"
assert_response :bad_request
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 40e32cb4d3..fe9ee6f73d 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
@@ -128,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
@@ -220,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
@@ -281,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
@@ -323,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
@@ -354,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
@@ -364,15 +423,181 @@ 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
- request = stub_request 'SERVER_SOFTWARE' => 'Apache3.422'
- assert_equal 'apache', request.server_software
+class LocalhostTest < BaseRequestTest
+ test "IPs that match localhost" do
+ request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1")
+ assert request.local?
+ 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
+
+ # 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
+
+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
- request = stub_request 'SERVER_SOFTWARE' => 'lighttpd(1.1.4)'
- assert_equal 'lighttpd', request.server_software
+ # 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
@@ -391,19 +616,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
@@ -411,63 +629,64 @@ 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 "method returns environment's request method when it has not been
+ overridden by middleware".squish do
+
+ ActionDispatch::Request::HTTP_METHODS.each do |method|
+ request = stub_request('REQUEST_METHOD' => method)
- 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, request.method_symbol
+ assert_equal method, request.method
+ assert_equal method.underscore.to_sym, 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
- assert_equal(method == "HEAD" ? "GET" : method, request.method)
- end
+ test "method returns original value of environment request method on POST" do
+ request = stub_request('rack.methodoverride.original_method' => 'POST')
+ assert_equal 'POST', request.method
end
- test "invalid method hacking on post raises exception" do
+ test "method raises exception on invalid HTTP method" do
assert_raise(ActionController::UnknownHttpMethod) do
- request = stub_request "REQUEST_METHOD" => "_RANDOM_METHOD"
- request.request_method
+ stub_request('rack.methodoverride.original_method' => '_RANDOM_METHOD').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' }
- assert_equal method.to_s.upcase, request.method
+ assert_raise(ActionController::UnknownHttpMethod) do
+ stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').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?
@@ -493,7 +712,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' })
@@ -513,109 +734,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
+ end
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
+ 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
@@ -630,6 +797,12 @@ class RequestTest < ActiveSupport::TestCase
assert_not request.format.json?
end
+ test "format does not throw exceptions when malformed parameters" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+ assert request.formats
+ assert request.format.html?
+ end
+
test "formats with xhr request" do
request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
request.expects(:parameters).at_least_once.returns({})
@@ -637,6 +810,7 @@ class RequestTest < ActiveSupport::TestCase
end
test "ignore_accept_header" do
+ old_ignore_accept_header = ActionDispatch::Request.ignore_accept_header
ActionDispatch::Request.ignore_accept_header = true
begin
@@ -666,33 +840,90 @@ class RequestTest < ActiveSupport::TestCase
request.expects(:parameters).at_least_once.returns({:format => :json})
assert_equal [ Mime::JSON ], request.formats
ensure
- ActionDispatch::Request.ignore_accept_header = false
+ ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header
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 not accessible after rack parse error" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+
+ 2.times do
+ assert_raises(ActionController::BadRequest) do
+ # rack will raise a TypeError when parsing this query string
+ request.parameters
+ end
+ end
+ 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'],
@@ -721,9 +952,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"]
@@ -732,10 +968,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
@@ -745,9 +985,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
@@ -755,56 +997,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
@@ -843,7 +1069,9 @@ class RequestTest < ActiveSupport::TestCase
assert request.etag_matches?(etag), etag
end
end
+end
+class RequestVariant < BaseRequestTest
test "setting variant" do
request = stub_request
@@ -868,16 +1096,4 @@ class RequestTest < ActiveSupport::TestCase
request.variant = "mobile"
end
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
- ActionDispatch::Request.new(env)
- end
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 959a3bc5cd..187b9a2420 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -178,6 +178,7 @@ class ResponseTest < ActiveSupport::TestCase
end
test "read x_frame_options, x_content_type_options and x_xss_protection" do
+ original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
'X-Frame-Options' => 'DENY',
@@ -193,11 +194,12 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal('nosniff', resp.headers['X-Content-Type-Options'])
assert_equal('1;', resp.headers['X-XSS-Protection'])
ensure
- ActionDispatch::Response.default_headers = nil
+ ActionDispatch::Response.default_headers = original_default_headers
end
end
test "read custom default_header" do
+ original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
'X-XX-XXXX' => 'Here is my phone number'
@@ -209,7 +211,7 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal('Here is my phone number', resp.headers['X-XX-XXXX'])
ensure
- ActionDispatch::Response.default_headers = nil
+ ActionDispatch::Response.default_headers = original_default_headers
end
end
diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb
index 9f37701656..7ef513b0c8 100644
--- a/actionpack/test/dispatch/routing/concerns_test.rb
+++ b/actionpack/test/dispatch/routing/concerns_test.rb
@@ -36,7 +36,8 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = RoutedRackApp.new Routes
+ def app; APP end
def test_accessing_concern_from_resources
get "/posts/1/comments"
diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb
index 0e488d2b88..c465d56bde 100644
--- a/actionpack/test/dispatch/routing/route_set_test.rb
+++ b/actionpack/test/dispatch/routing/route_set_test.rb
@@ -81,10 +81,6 @@ module ActionDispatch
end
private
- def clear!
- @set.clear!
- end
-
def draw(&block)
@set.draw(&block)
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 1fa2cc6cf2..b8e20c52a0 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -99,6 +99,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
+ def test_namespace_without_controller_segment
+ draw do
+ namespace :admin do
+ get 'hello/:controllers/:action'
+ end
+ end
+ get '/admin/hello/foo/new'
+ assert_equal 'foo', @request.params["controllers"]
+ end
+
def test_session_singleton_resource
draw do
resource :session do
@@ -351,8 +361,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
draw do
controller(:global) do
get 'global/hide_notice'
- get 'global/export', :to => :export, :as => :export_request
- get '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ }
+ get 'global/export', :action => :export, :as => :export_request
+ get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ }
get 'global/:action'
end
end
@@ -720,8 +730,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
draw do
resources :replies do
member do
- put :answer, :to => :mark_as_answer
- delete :answer, :to => :unmark_as_answer
+ put :answer, :action => :mark_as_answer
+ delete :answer, :action => :unmark_as_answer
end
end
end
@@ -1031,6 +1041,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
@@ -1048,7 +1188,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
controller :articles do
scope '/articles', :as => 'article' do
scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do
- get '/:id', :to => :with_id, :as => ""
+ get '/:id', :action => :with_id, :as => ""
end
end
end
@@ -1295,7 +1435,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_scoped_controller_with_namespace_and_action
draw do
namespace :account do
- get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback
+ get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback
end
end
@@ -1352,7 +1492,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_normalize_namespaced_matches
draw do
namespace :account do
- get 'description', :to => :description, :as => "description"
+ get 'description', :action => :description, :as => "description"
end
end
@@ -1593,7 +1733,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "whatever/:controller(/:action(/:id))"
end
- get 'whatever/foo/bar'
+ get '/whatever/foo/bar'
assert_equal 'foo#bar', @response.body
assert_equal 'http://www.example.com/whatever/foo/bar/1',
@@ -1605,10 +1745,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "whatever/:controller(/:action(/:id))", :id => /\d+/
end
- get 'whatever/foo/bar/show'
+ get '/whatever/foo/bar/show'
assert_equal 'foo/bar#show', @response.body
- get 'whatever/foo/bar/show/1'
+ get '/whatever/foo/bar/show/1'
assert_equal 'foo/bar#show', @response.body
assert_equal 'http://www.example.com/whatever/foo/bar/show',
@@ -1828,6 +1968,82 @@ 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_direct_children_of_shallow_resources
+ draw do
+ resources :blogs do
+ resources :posts, shallow: true do
+ resources :comments
+ end
+ end
+ end
+
+ post '/posts/1/comments'
+ assert_equal 'comments#create', @response.body
+ assert_equal '/posts/1/comments', post_comments_path('1')
+
+ get '/posts/2/comments/new'
+ assert_equal 'comments#new', @response.body
+ assert_equal '/posts/2/comments/new', new_post_comment_path('2')
+
+ get '/posts/1/comments'
+ assert_equal 'comments#index', @response.body
+ assert_equal '/posts/1/comments', post_comments_path('1')
+ end
+
def test_shallow_nested_resources_within_scope
draw do
scope '/hello' do
@@ -1960,7 +2176,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
resources :invoices do
get "outstanding" => "invoices#outstanding", :on => :collection
- get "overdue", :to => :overdue, :on => :collection
+ get "overdue", :action => :overdue, :on => :collection
get "print" => "invoices#print", :as => :print, :on => :member
post "preview" => "invoices#preview", :as => :preview, :on => :new
end
@@ -2048,6 +2264,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml)
end
+ def test_match_without_via
+ assert_raises(ArgumentError) do
+ draw do
+ match '/foo/bar', :to => 'files#show'
+ end
+ end
+ end
+
+ def test_match_with_empty_via
+ assert_raises(ArgumentError) do
+ draw do
+ match '/foo/bar', :to => 'files#show', :via => []
+ end
+ end
+ end
+
def test_glob_parameter_accepts_regexp
draw do
get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/
@@ -2103,12 +2335,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "(/user/:username)/photos" => "photos#index"
end
- get 'user/bob/photos'
+ get '/user/bob/photos'
assert_equal 'photos#index', @response.body
assert_equal 'http://www.example.com/user/bob/photos',
url_for(:controller => "photos", :action => "index", :username => "bob")
- get 'photos'
+ get '/photos'
assert_equal 'photos#index', @response.body
assert_equal 'http://www.example.com/photos',
url_for(:controller => "photos", :action => "index", :username => nil)
@@ -2621,7 +2853,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- def test_symbolized_path_parameters_is_not_stale
+ def test_path_parameters_is_not_stale
draw do
scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do
get '/', :to => 'countries#index'
@@ -2786,7 +3018,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
assert_raise(ArgumentError) do
- draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
+ assert_deprecated do
+ draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
+ end
end
assert_raise(ArgumentError) do
@@ -2936,7 +3170,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get '/downloads/1/1.tar'
assert_equal 'downloads#show', @response.body
- assert_equal expected_params, @request.symbolized_path_parameters
+ assert_equal expected_params, @request.path_parameters
assert_equal '/downloads/1/1.tar', download_path('1')
assert_equal '/downloads/1/1.tar', download_path('1', '1')
end
@@ -2953,6 +3187,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/foo', foo_root_path
end
+ def test_namespace_as_controller
+ 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
@@ -3033,23 +3279,183 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/admin/posts/1/comments', admin_post_comments_path('1')
end
+ def test_mix_string_to_controller_action
+ draw do
+ get '/projects', controller: 'project_files',
+ action: 'index',
+ to: 'comments#index'
+ end
+ get '/projects'
+ assert_equal 'comments#index', @response.body
+ end
+
+ def test_mix_string_to_controller
+ draw do
+ get '/projects', controller: 'project_files',
+ to: 'comments#index'
+ end
+ get '/projects'
+ assert_equal 'comments#index', @response.body
+ end
+
+ def test_mix_string_to_action
+ draw do
+ get '/projects', action: 'index',
+ to: 'comments#index'
+ end
+ get '/projects'
+ assert_equal 'comments#index', @response.body
+ end
+
+ def test_mix_symbol_to_controller_action
+ assert_deprecated do
+ draw do
+ get '/projects', controller: 'project_files',
+ action: 'index',
+ to: :show
+ end
+ end
+ get '/projects'
+ assert_equal 'project_files#show', @response.body
+ end
+
+ def test_mix_string_to_controller_action_no_hash
+ assert_deprecated do
+ draw do
+ get '/projects', controller: 'project_files',
+ action: 'index',
+ to: 'show'
+ end
+ end
+ get '/projects'
+ assert_equal 'show#index', @response.body
+ 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)
self.class.stub_controllers do |routes|
- @app = routes
- @app.default_url_options = { host: 'www.example.com' }
- @app.draw(&block)
+ routes.default_url_options = { host: 'www.example.com' }
+ routes.draw(&block)
+ @app = RoutedRackApp.new routes
end
end
def url_for(options = {})
- @app.url_helpers.url_for(options)
+ @app.routes.url_helpers.url_for(options)
end
def method_missing(method, *args, &block)
if method.to_s =~ /_(path|url)$/
- @app.url_helpers.send(method, *args, &block)
+ @app.routes.url_helpers.send(method, *args, &block)
else
super
end
@@ -3076,12 +3482,14 @@ end
class TestAltApp < ActionDispatch::IntegrationTest
class AltRequest
+ attr_accessor :path_parameters, :path_info, :script_name
+ attr_reader :env
+
def initialize(env)
+ @path_parameters = {}
@env = env
- end
-
- def path_info
- "/"
+ @path_info = "/"
+ @script_name = ""
end
def request_method
@@ -3115,8 +3523,10 @@ class TestAltApp < ActionDispatch::IntegrationTest
get "/" => TestAltApp::AltApp.new
end
+ APP = build_app AltRoutes
+
def app
- AltRoutes
+ APP
end
def test_alt_request_without_header
@@ -3143,15 +3553,16 @@ class TestAppendingRoutes < ActionDispatch::IntegrationTest
def setup
super
s = self
- @app = ActionDispatch::Routing::RouteSet.new
- @app.append do
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.append do
get '/hello' => s.simple_app('fail')
get '/goodbye' => s.simple_app('goodbye')
end
- @app.draw do
+ routes.draw do
get '/hello' => s.simple_app('hello')
end
+ @app = self.class.build_app routes
end
def test_goodbye_should_be_available
@@ -3180,8 +3591,38 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
end
def draw(&block)
- @app = ActionDispatch::Routing::RouteSet.new
- @app.draw(&block)
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.draw(&block)
+ @app = self.class.build_app routes
+ end
+
+ def test_missing_controller
+ ex = assert_raises(ArgumentError) {
+ draw do
+ get '/foo/bar', :action => :index
+ end
+ }
+ assert_match(/Missing :controller/, ex.message)
+ end
+
+ def test_missing_action
+ ex = assert_raises(ArgumentError) {
+ assert_deprecated do
+ draw do
+ get '/foo/bar', :to => 'foo'
+ end
+ end
+ }
+ assert_match(/Missing :action/, ex.message)
+ end
+
+ def test_missing_action_on_hash
+ ex = assert_raises(ArgumentError) {
+ draw do
+ get '/foo/bar', :to => 'foo#'
+ end
+ }
+ assert_match(/Missing :action/, ex.message)
end
def test_valid_controller_options_inside_namespace
@@ -3200,7 +3641,7 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
resources :storage_files, :controller => 'admin/storage_files'
end
- get 'storage_files'
+ get '/storage_files'
assert_equal "admin/storage_files#index", @response.body
end
@@ -3225,6 +3666,16 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
assert_match "'Admin::StorageFiles' is not a supported controller name", e.message
end
+
+ def test_warn_with_ruby_constant_syntax_no_colons
+ e = assert_raise(ArgumentError) do
+ draw do
+ resources :storage_files, :controller => 'Admin'
+ end
+ end
+
+ assert_match "'Admin' is not a supported controller name", e.message
+ end
end
class TestDefaultScope < ActionDispatch::IntegrationTest
@@ -3242,8 +3693,10 @@ class TestDefaultScope < ActionDispatch::IntegrationTest
resources :posts
end
+ APP = build_app DefaultScopeRoutes
+
def app
- DefaultScopeRoutes
+ APP
end
include DefaultScopeRoutes.url_helpers
@@ -3261,24 +3714,28 @@ class TestHttpMethods < ActionDispatch::IntegrationTest
RFC3648 = %w(ORDERPATCH)
RFC3744 = %w(ACL)
RFC5323 = %w(SEARCH)
+ RFC4791 = %w(MKCALENDAR)
RFC5789 = %w(PATCH)
def simple_app(response)
lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] }
end
- setup do
+ attr_reader :app
+
+ def setup
s = self
- @app = ActionDispatch::Routing::RouteSet.new
+ routes = ActionDispatch::Routing::RouteSet.new
+ @app = RoutedRackApp.new routes
- @app.draw do
- (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method|
+ routes.draw do
+ (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method|
match '/' => s.simple_app(method), :via => method.underscore.to_sym
end
end
end
- (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method|
+ (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method|
test "request method #{method.underscore} can be matched" do
get '/', nil, 'REQUEST_METHOD' => method
assert_equal method, @response.body
@@ -3302,10 +3759,11 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP 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
@@ -3313,7 +3771,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
@@ -3333,7 +3791,8 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test 'recognizes unicode path' do
get "/#{Rack::Utils.escape("ほげ")}"
@@ -3364,7 +3823,8 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "controller option which starts with '/' from multiple nested controller" do
get "/foo/bar/baz"
@@ -3383,7 +3843,8 @@ class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test 'recognizes tilde path' do
get "/~user"
@@ -3410,7 +3871,8 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "redirect escapes interpolated parameters with redirect proc" do
get "/foo/1%3E"
@@ -3452,7 +3914,8 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "parameters are reset between constraint checks" do
get "/bar"
@@ -3472,7 +3935,8 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest
end
#include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
def test_glob_constraint
get "/dummy"
@@ -3498,11 +3962,14 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
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
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test 'enabled when not mounted and default_url_options is empty' do
assert Routes.url_helpers.optimize_routes_generation?
@@ -3530,6 +3997,26 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
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
@@ -3554,7 +4041,8 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
include Routes.url_helpers
@@ -3589,7 +4077,8 @@ class TestUrlConstraints < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "constraints are copied to defaults when using constraints method" do
assert_equal 'http://admin.example.com/', admin_root_url
@@ -3643,7 +4132,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest
set.draw do
get "/bar/:id", :to => redirect("/foo/show/%{id}")
get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show"
- get "/foo(/:action(/:id))", :to => "test_invalid_urls/foo"
+ get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo"
get "/:controller(/:action(/:id))"
end
@@ -3670,8 +4159,9 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest
end
end
+ APP = build_app Routes
def app
- Routes
+ APP
end
include Routes.url_helpers
@@ -3702,7 +4192,8 @@ class TestPortConstraints < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
def test_integer_port_constraints
get 'http://www.example.com/integer'
@@ -3750,7 +4241,8 @@ class TestFormatConstraints < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
def test_string_format_constraints
get 'http://www.example.com/string'
@@ -3797,6 +4289,17 @@ class TestFormatConstraints < ActionDispatch::IntegrationTest
end
end
+class TestCallableConstraintValidation < ActionDispatch::IntegrationTest
+ def test_constraint_with_object_not_callable
+ assert_raises(ArgumentError) do
+ ActionDispatch::Routing::RouteSet.new.draw do
+ ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ get '/test', to: ok, constraints: Object.new
+ end
+ end
+ end
+end
+
class TestRouteDefaults < ActionDispatch::IntegrationTest
stub_controllers do |routes|
Routes = routes
@@ -3806,8 +4309,9 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest
end
end
+ APP = build_app Routes
def app
- Routes
+ APP
end
include Routes.url_helpers
@@ -3835,8 +4339,9 @@ class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest
end
end
+ APP = build_app Routes
def app
- Routes
+ APP
end
include Routes.url_helpers
@@ -3861,8 +4366,9 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest
end
end
+ APP = build_app Routes
def app
- Routes
+ APP
end
include Routes.url_helpers
@@ -3885,7 +4391,8 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
include Routes.url_helpers
diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb
index b8479e8836..9f810cad01 100644
--- a/actionpack/test/dispatch/session/cache_store_test.rb
+++ b/actionpack/test/dispatch/session/cache_store_test.rb
@@ -148,16 +148,15 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_prevents_session_fixation
with_test_route_set do
- get '/get_session_value'
- assert_response :success
- assert_equal 'foo: nil', response.body
- session_id = cookies['_session_id']
+ assert_equal nil, @cache.read('_session_id:0xhax')
- reset!
+ cookies['_session_id'] = '0xhax'
+ get '/set_session_value'
- get '/set_session_value', :_session_id => session_id
assert_response :success
- assert_not_equal session_id, cookies['_session_id']
+ assert_not_equal '0xhax', cookies['_session_id']
+ assert_equal nil, @cache.read('_session_id:0xhax')
+ assert_equal({'foo' => 'bar'}, @cache.read("_session_id:#{cookies['_session_id']}"))
end
end
@@ -169,8 +168,8 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
end
@app = self.class.build_app(set) do |middleware|
- cache = ActiveSupport::Cache::MemoryStore.new
- middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => cache
+ @cache = ActiveSupport::Cache::MemoryStore.new
+ middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache
middleware.delete "ActionDispatch::ShowExceptions"
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..f7a06cfed4 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
@@ -48,6 +49,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal 'foo: "bar"', response.body
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_nil_session_value
@@ -56,6 +59,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal 'foo: nil', response.body
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_session_value_after_session_reset
@@ -75,6 +80,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_from_nonexistent_session
@@ -84,6 +91,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_equal 'foo: nil', response.body
assert_nil cookies['_session_id'], "should only create session on write, not read"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_setting_session_value_after_session_reset
@@ -105,6 +114,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_not_equal session_id, response.body
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_session_id
@@ -118,6 +129,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_deserializes_unloaded_class
@@ -132,6 +145,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
end
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_doesnt_write_session_cookie_if_session_id_is_already_exists
@@ -144,6 +159,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_prevents_session_fixation
@@ -159,6 +176,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_not_equal session_id, cookies['_session_id']
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
rescue LoadError, RuntimeError, Dalli::DalliError
$stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again."
@@ -172,7 +191,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/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb
index 38bd234f37..323fbc285e 100644
--- a/actionpack/test/dispatch/show_exceptions_test.rb
+++ b/actionpack/test/dispatch/show_exceptions_test.rb
@@ -37,7 +37,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
get "/", {}, {'action_dispatch.show_exceptions' => true}
assert_response 500
assert_equal "500 error fixture\n", body
-
+
get "/bad_params", {}, {'action_dispatch.show_exceptions' => true}
assert_response 400
assert_equal "400 error fixture\n", body
@@ -92,6 +92,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
exceptions_app = lambda do |env|
assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"]
assert_equal "/404", env["PATH_INFO"]
+ assert_equal "/not_found_original_exception", env["action_dispatch.original_path"]
[404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]]
end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 94969f795a..c3598c5e8e 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -196,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/template_assertions_test.rb b/actionpack/test/dispatch/template_assertions_test.rb
new file mode 100644
index 0000000000..3c393f937b
--- /dev/null
+++ b/actionpack/test/dispatch/template_assertions_test.rb
@@ -0,0 +1,98 @@
+require 'abstract_unit'
+
+class AssertTemplateController < ActionController::Base
+ def render_with_partial
+ render partial: 'test/partial'
+ end
+
+ def render_with_template
+ render 'test/hello_world'
+ end
+
+ def render_with_layout
+ @variable_for_layout = nil
+ render 'test/hello_world', layout: "layouts/standard"
+ end
+
+ def render_with_file
+ render file: 'README.rdoc'
+ end
+
+ def render_nothing
+ head :ok
+ end
+end
+
+class AssertTemplateControllerTest < ActionDispatch::IntegrationTest
+ def test_template_reset_between_requests
+ get '/assert_template/render_with_template'
+ assert_template 'test/hello_world'
+
+ get '/assert_template/render_nothing'
+ assert_template nil
+ end
+
+ def test_partial_reset_between_requests
+ get '/assert_template/render_with_partial'
+ assert_template partial: 'test/_partial'
+
+ get '/assert_template/render_nothing'
+ assert_template partial: nil
+ end
+
+ def test_layout_reset_between_requests
+ get '/assert_template/render_with_layout'
+ assert_template layout: 'layouts/standard'
+
+ get '/assert_template/render_nothing'
+ assert_template layout: nil
+ end
+
+ def test_file_reset_between_requests
+ get '/assert_template/render_with_file'
+ assert_template file: 'README.rdoc'
+
+ get '/assert_template/render_nothing'
+ assert_template file: nil
+ end
+
+ def test_template_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_template'
+ session.assert_template 'test/hello_world'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template nil
+ end
+ end
+
+ def test_partial_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_partial'
+ session.assert_template partial: 'test/_partial'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template partial: nil
+ end
+ end
+
+ def test_layout_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_layout'
+ session.assert_template layout: 'layouts/standard'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template layout: nil
+ end
+ end
+
+ def test_file_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_file'
+ session.assert_template file: 'README.rdoc'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template file: nil
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb
index 72f3d1db0d..55ebbd5143 100644
--- a/actionpack/test/dispatch/uploaded_file_test.rb
+++ b/actionpack/test/dispatch/uploaded_file_test.rb
@@ -18,6 +18,12 @@ module ActionDispatch
assert_equal "UTF-8", uf.original_filename.encoding.to_s
end
+ def test_filename_should_always_be_in_utf_8
+ uf = Http::UploadedFile.new(:filename => 'foo'.encode(Encoding::SHIFT_JIS),
+ :tempfile => Object.new)
+ assert_equal "UTF-8", uf.original_filename.encoding.to_s
+ end
+
def test_content_type
uf = Http::UploadedFile.new(:type => 'foo', :tempfile => Object.new)
assert_equal 'foo', uf.content_type
@@ -33,6 +39,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 fdea27e2d2..8f79e7bf9a 100644
--- a/actionpack/test/dispatch/url_generation_test.rb
+++ b/actionpack/test/dispatch/url_generation_test.rb
@@ -15,15 +15,19 @@ module TestUrlGeneration
Routes.draw do
get "/foo", :to => "my_route_generating#index", :as => :foo
+ resources :bars
+
mount MyRouteGeneratingController.action(:index), at: '/bar'
end
+ APP = build_app Routes
+
def _routes
Routes
end
def app
- Routes
+ APP
end
test "generating URLS normally" do
@@ -64,18 +68,30 @@ module TestUrlGeneration
test "port is extracted from the host" do
assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "http://")
+ assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "//")
+ assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:80", protocol: "//")
+ end
+
+ test "port option is used" do
+ assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "http://", port: 8080)
+ assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "//", port: 8080)
+ assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com", protocol: "//", port: 80)
end
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)
+ assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: 8080)
+ assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:443", protocol: "//", port: 80)
end
test "port option disables the host when set to nil" do
assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: nil)
+ assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: nil)
end
test "port option disables the host when set to false" do
assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: false)
+ assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: false)
end
test "keep subdomain when key is true" do
@@ -97,6 +113,22 @@ module TestUrlGeneration
test "omit subdomain when key is blank" do
assert_equal "http://example.com/foo", foo_url(subdomain: "")
end
+
+ test "generating URLs with trailing slashes" do
+ assert_equal "/bars.json", bars_path(
+ trailing_slash: true,
+ format: 'json'
+ )
+ end
+
+ test "generating URLS with querystring and trailing slashes" do
+ assert_equal "/bars.json?a=b", bars_path(
+ trailing_slash: true,
+ a: 'b',
+ format: 'json'
+ )
+ end
+
end
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/test/_changing_priority.html.erb b/actionpack/test/fixtures/test/_changing_priority.html.erb
deleted file mode 100644
index 3225efc49a..0000000000
--- a/actionpack/test/fixtures/test/_changing_priority.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-HTML \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_changing_priority.json.erb b/actionpack/test/fixtures/test/_changing_priority.json.erb
deleted file mode 100644
index 7fa41dce66..0000000000
--- a/actionpack/test/fixtures/test/_changing_priority.json.erb
+++ /dev/null
@@ -1 +0,0 @@
-JSON \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_counter.html.erb b/actionpack/test/fixtures/test/_counter.html.erb
deleted file mode 100644
index fd245bfc70..0000000000
--- a/actionpack/test/fixtures/test/_counter.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= counter_counter %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer.erb b/actionpack/test/fixtures/test/_customer.erb
deleted file mode 100644
index d8220afeda..0000000000
--- a/actionpack/test/fixtures/test/_customer.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello: <%= customer.name rescue "Anonymous" %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer_counter.erb b/actionpack/test/fixtures/test/_customer_counter.erb
deleted file mode 100644
index 3435979dba..0000000000
--- a/actionpack/test/fixtures/test/_customer_counter.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= customer_counter.name %><%= customer_counter_counter %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer_counter_with_as.erb b/actionpack/test/fixtures/test/_customer_counter_with_as.erb
deleted file mode 100644
index 1241eb604d..0000000000
--- a/actionpack/test/fixtures/test/_customer_counter_with_as.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= client.name %><%= client_counter %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer_greeting.erb b/actionpack/test/fixtures/test/_customer_greeting.erb
deleted file mode 100644
index 6acbcb20c4..0000000000
--- a/actionpack/test/fixtures/test/_customer_greeting.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= greeting %>: <%= customer_greeting.name %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer_with_var.erb b/actionpack/test/fixtures/test/_customer_with_var.erb
deleted file mode 100644
index 00047dd20e..0000000000
--- a/actionpack/test/fixtures/test/_customer_with_var.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= customer.name %> <%= customer.name %> <%= customer.name %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb
deleted file mode 100644
index 1cc8d41475..0000000000
--- a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello <%= name %>
diff --git a/actionpack/test/fixtures/test/_first_json_partial.json.erb b/actionpack/test/fixtures/test/_first_json_partial.json.erb
deleted file mode 100644
index 790ee896db..0000000000
--- a/actionpack/test/fixtures/test/_first_json_partial.json.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render :partial => "test/second_json_partial" %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_form.erb b/actionpack/test/fixtures/test/_form.erb
deleted file mode 100644
index 01107f1cb2..0000000000
--- a/actionpack/test/fixtures/test/_form.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= form.label :title %>
diff --git a/actionpack/test/fixtures/test/_hash_greeting.erb b/actionpack/test/fixtures/test/_hash_greeting.erb
deleted file mode 100644
index fc54a36f2a..0000000000
--- a/actionpack/test/fixtures/test/_hash_greeting.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= greeting %>: <%= hash_greeting[:first_name] %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_hash_object.erb b/actionpack/test/fixtures/test/_hash_object.erb
deleted file mode 100644
index 34a92c6a56..0000000000
--- a/actionpack/test/fixtures/test/_hash_object.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<%= hash_object[:first_name] %>
-<%= hash_object[:first_name].reverse %>
diff --git a/actionpack/test/fixtures/test/_hello.builder b/actionpack/test/fixtures/test/_hello.builder
deleted file mode 100644
index ef52f632d1..0000000000
--- a/actionpack/test/fixtures/test/_hello.builder
+++ /dev/null
@@ -1 +0,0 @@
-xm.hello \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_json_change_priority.json.erb b/actionpack/test/fixtures/test/_json_change_priority.json.erb
deleted file mode 100644
index e69de29bb2..0000000000
--- a/actionpack/test/fixtures/test/_json_change_priority.json.erb
+++ /dev/null
diff --git a/actionpack/test/fixtures/test/_labelling_form.erb b/actionpack/test/fixtures/test/_labelling_form.erb
deleted file mode 100644
index 1b95763165..0000000000
--- a/actionpack/test/fixtures/test/_labelling_form.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= labelling_form.label :title %>
diff --git a/actionpack/test/fixtures/test/_layout_for_partial.html.erb b/actionpack/test/fixtures/test/_layout_for_partial.html.erb
deleted file mode 100644
index 666efadbb6..0000000000
--- a/actionpack/test/fixtures/test/_layout_for_partial.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-Before (<%= name %>)
-<%= yield %>
-After \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb b/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb
deleted file mode 100644
index 3a03a64e31..0000000000
--- a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Inside from partial (<%= name %>) \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_partial_html_erb.html.erb b/actionpack/test/fixtures/test/_partial_html_erb.html.erb
deleted file mode 100644
index 4b54875782..0000000000
--- a/actionpack/test/fixtures/test/_partial_html_erb.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= "partial.html.erb" %>
diff --git a/actionpack/test/fixtures/test/_partial_name_local_variable.erb b/actionpack/test/fixtures/test/_partial_name_local_variable.erb
deleted file mode 100644
index cc3a91c89f..0000000000
--- a/actionpack/test/fixtures/test/_partial_name_local_variable.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= partial_name_local_variable %>
diff --git a/actionpack/test/fixtures/test/_partial_only.erb b/actionpack/test/fixtures/test/_partial_only.erb
deleted file mode 100644
index a44b3eed40..0000000000
--- a/actionpack/test/fixtures/test/_partial_only.erb
+++ /dev/null
@@ -1 +0,0 @@
-only partial \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_partial_only_html.html b/actionpack/test/fixtures/test/_partial_only_html.html
deleted file mode 100644
index d2d630bd40..0000000000
--- a/actionpack/test/fixtures/test/_partial_only_html.html
+++ /dev/null
@@ -1 +0,0 @@
-only html partial \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_partial_with_partial.erb b/actionpack/test/fixtures/test/_partial_with_partial.erb
deleted file mode 100644
index ee0d5037b6..0000000000
--- a/actionpack/test/fixtures/test/_partial_with_partial.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<%= render 'test/partial' %>
-partial with partial
diff --git a/actionpack/test/fixtures/test/_person.erb b/actionpack/test/fixtures/test/_person.erb
deleted file mode 100644
index b2e5688956..0000000000
--- a/actionpack/test/fixtures/test/_person.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-Second: <%= name %>
-Third: <%= @name %>
diff --git a/actionpack/test/fixtures/test/_raise_indentation.html.erb b/actionpack/test/fixtures/test/_raise_indentation.html.erb
deleted file mode 100644
index f9a93728fe..0000000000
--- a/actionpack/test/fixtures/test/_raise_indentation.html.erb
+++ /dev/null
@@ -1,13 +0,0 @@
-<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/actionpack/test/fixtures/test/_second_json_partial.json.erb b/actionpack/test/fixtures/test/_second_json_partial.json.erb
deleted file mode 100644
index 5ebb7f1afd..0000000000
--- a/actionpack/test/fixtures/test/_second_json_partial.json.erb
+++ /dev/null
@@ -1 +0,0 @@
-Third level \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/action_talk_to_layout.erb b/actionpack/test/fixtures/test/action_talk_to_layout.erb
deleted file mode 100644
index 36e896daa8..0000000000
--- a/actionpack/test/fixtures/test/action_talk_to_layout.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<% @title = "Talking to the layout" -%>
-Action was here! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb b/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb
deleted file mode 100644
index ac44bc0d81..0000000000
--- a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/capturing.erb b/actionpack/test/fixtures/test/capturing.erb
deleted file mode 100644
index 1addaa40d9..0000000000
--- a/actionpack/test/fixtures/test/capturing.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-<% days = capture do %>
- Dreamy days
-<% end %>
-<%= days %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/change_priority.html.erb b/actionpack/test/fixtures/test/change_priority.html.erb
deleted file mode 100644
index 5618977d05..0000000000
--- a/actionpack/test/fixtures/test/change_priority.html.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<%= 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/actionpack/test/fixtures/test/content_for.erb b/actionpack/test/fixtures/test/content_for.erb
deleted file mode 100644
index 1fb829f54c..0000000000
--- a/actionpack/test/fixtures/test/content_for.erb
+++ /dev/null
@@ -1 +0,0 @@
-<% content_for :title do -%>Putting stuff in the title!<% end -%>Great stuff! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/content_for_concatenated.erb b/actionpack/test/fixtures/test/content_for_concatenated.erb
deleted file mode 100644
index e65f629574..0000000000
--- a/actionpack/test/fixtures/test/content_for_concatenated.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-<% content_for :title, "Putting stuff "
- content_for :title, "in the title!" -%>
-Great stuff! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/content_for_with_parameter.erb b/actionpack/test/fixtures/test/content_for_with_parameter.erb
deleted file mode 100644
index aeb6f73ce0..0000000000
--- a/actionpack/test/fixtures/test/content_for_with_parameter.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<% content_for :title, "Putting stuff in the title!" -%>
-Great stuff! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/formatted_html_erb.html.erb b/actionpack/test/fixtures/test/formatted_html_erb.html.erb
deleted file mode 100644
index 1c64efabd8..0000000000
--- a/actionpack/test/fixtures/test/formatted_html_erb.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-formatted html erb \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/greeting.html.erb b/actionpack/test/fixtures/test/greeting.html.erb
deleted file mode 100644
index 62fb0293f0..0000000000
--- a/actionpack/test/fixtures/test/greeting.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<p>This is grand!</p>
diff --git a/actionpack/test/fixtures/test/greeting.xml.erb b/actionpack/test/fixtures/test/greeting.xml.erb
deleted file mode 100644
index 62fb0293f0..0000000000
--- a/actionpack/test/fixtures/test/greeting.xml.erb
+++ /dev/null
@@ -1 +0,0 @@
-<p>This is grand!</p>
diff --git a/actionpack/test/fixtures/test/hello,world.erb b/actionpack/test/fixtures/test/hello,world.erb
deleted file mode 100644
index bc8fa5e0ca..0000000000
--- a/actionpack/test/fixtures/test/hello,world.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello w*rld! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello.builder b/actionpack/test/fixtures/test/hello.builder
deleted file mode 100644
index a471553941..0000000000
--- a/actionpack/test/fixtures/test/hello.builder
+++ /dev/null
@@ -1,4 +0,0 @@
-xml.html do
- xml.p "Hello #{@name}"
- xml << render(:file => "test/greeting")
-end \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello_world_container.builder b/actionpack/test/fixtures/test/hello_world_container.builder
deleted file mode 100644
index e48d75c405..0000000000
--- a/actionpack/test/fixtures/test/hello_world_container.builder
+++ /dev/null
@@ -1,3 +0,0 @@
-xml.test do
- render :partial => 'hello', :locals => { :xm => xml }
-end \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello_world_from_rxml.builder b/actionpack/test/fixtures/test/hello_world_from_rxml.builder
deleted file mode 100644
index 619a97ba96..0000000000
--- a/actionpack/test/fixtures/test/hello_world_from_rxml.builder
+++ /dev/null
@@ -1,3 +0,0 @@
-xml.html do
- xml.p "Hello"
-end
diff --git a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb b/actionpack/test/fixtures/test/hello_world_with_layout_false.erb
deleted file mode 100644
index 6769dd60bd..0000000000
--- a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello world! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/html_template.html.erb b/actionpack/test/fixtures/test/html_template.html.erb
deleted file mode 100644
index 1bbc2b7f09..0000000000
--- a/actionpack/test/fixtures/test/html_template.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render :partial => "test/first_json_partial", formats: :json %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hyphen-ated.erb b/actionpack/test/fixtures/test/hyphen-ated.erb
deleted file mode 100644
index cd0875583a..0000000000
--- a/actionpack/test/fixtures/test/hyphen-ated.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello world!
diff --git a/actionpack/test/fixtures/test/list.erb b/actionpack/test/fixtures/test/list.erb
deleted file mode 100644
index 0a4bda58ee..0000000000
--- a/actionpack/test/fixtures/test/list.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %>
diff --git a/actionpack/test/fixtures/test/non_erb_block_content_for.builder b/actionpack/test/fixtures/test/non_erb_block_content_for.builder
deleted file mode 100644
index d539a425a4..0000000000
--- a/actionpack/test/fixtures/test/non_erb_block_content_for.builder
+++ /dev/null
@@ -1,4 +0,0 @@
-content_for :title do
- 'Putting stuff in the title!'
-end
-xml << "Great stuff!"
diff --git a/actionpack/test/fixtures/test/potential_conflicts.erb b/actionpack/test/fixtures/test/potential_conflicts.erb
deleted file mode 100644
index a5e964e359..0000000000
--- a/actionpack/test/fixtures/test/potential_conflicts.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-First: <%= @name %>
-<%= render :partial => "person", :locals => { :name => "Stephan" } -%>
-Fourth: <%= @name %>
-Fifth: <%= name %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/proper_block_detection.erb b/actionpack/test/fixtures/test/proper_block_detection.erb
deleted file mode 100644
index b55efbb25d..0000000000
--- a/actionpack/test/fixtures/test/proper_block_detection.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= @todo %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/render_file_from_template.html.erb b/actionpack/test/fixtures/test/render_file_from_template.html.erb
deleted file mode 100644
index fde9f4bb64..0000000000
--- a/actionpack/test/fixtures/test/render_file_from_template.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render :file => @path %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb b/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb
deleted file mode 100644
index 9b4900acc5..0000000000
--- a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= secret ||= 'one' %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb
deleted file mode 100644
index 0740b2d07c..0000000000
--- a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hey HTML!
diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb
deleted file mode 100644
index 4a11845cfe..0000000000
--- a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hello HTML! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb b/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb
deleted file mode 100644
index 892ae5eca2..0000000000
--- a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb
+++ /dev/null
@@ -1 +0,0 @@
-alert('hello'); \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb
deleted file mode 100644
index 1461b95186..0000000000
--- a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %>
diff --git a/actionpack/test/fixtures/test/render_to_string_test.erb b/actionpack/test/fixtures/test/render_to_string_test.erb
deleted file mode 100644
index 6e267e8634..0000000000
--- a/actionpack/test/fixtures/test/render_to_string_test.erb
+++ /dev/null
@@ -1 +0,0 @@
-The value of foo is: ::<%= @foo %>::
diff --git a/actionpack/test/fixtures/test/render_two_partials.html.erb b/actionpack/test/fixtures/test/render_two_partials.html.erb
deleted file mode 100644
index 3db6025860..0000000000
--- a/actionpack/test/fixtures/test/render_two_partials.html.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<%= render :partial => 'partial', :locals => {'first' => '1'} %>
-<%= render :partial => 'partial', :locals => {'second' => '2'} %>
diff --git a/actionpack/test/fixtures/test/using_layout_around_block.html.erb b/actionpack/test/fixtures/test/using_layout_around_block.html.erb
deleted file mode 100644
index 3d6661df9a..0000000000
--- a/actionpack/test/fixtures/test/using_layout_around_block.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render(:layout => "layout_for_partial", :locals => { :name => "David" }) do %>Inside from block<% end %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/with_html_partial.html.erb b/actionpack/test/fixtures/test/with_html_partial.html.erb
deleted file mode 100644
index d84d909d64..0000000000
--- a/actionpack/test/fixtures/test/with_html_partial.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<strong><%= render :partial => "partial_only_html" %></strong>
diff --git a/actionpack/test/fixtures/test/with_partial.html.erb b/actionpack/test/fixtures/test/with_partial.html.erb
deleted file mode 100644
index 7502364cf5..0000000000
--- a/actionpack/test/fixtures/test/with_partial.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<strong><%= render :partial => "partial_only" %></strong>
diff --git a/actionpack/test/fixtures/test/with_partial.text.erb b/actionpack/test/fixtures/test/with_partial.text.erb
deleted file mode 100644
index 5f068ebf27..0000000000
--- a/actionpack/test/fixtures/test/with_partial.text.erb
+++ /dev/null
@@ -1 +0,0 @@
-**<%= render :partial => "partial_only" %>**
diff --git a/actionpack/test/fixtures/test/with_xml_template.html.erb b/actionpack/test/fixtures/test/with_xml_template.html.erb
deleted file mode 100644
index e54a7cd001..0000000000
--- a/actionpack/test/fixtures/test/with_xml_template.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= render :template => "test/greeting", :formats => :xml %>
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index ce02104181..9dfdfc23ed 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -18,7 +18,7 @@ module ActionDispatch
'/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z},
}.each do |path, expected|
define_method(:"test_to_regexp_#{path}") do
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
path,
{ :controller => /.+/ },
["/", ".", "?"]
@@ -41,7 +41,7 @@ module ActionDispatch
'/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar},
}.each do |path, expected|
define_method(:"test_to_non_anchored_regexp_#{path}") do
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
path,
{ :controller => /.+/ },
["/", ".", "?"],
@@ -65,7 +65,7 @@ module ActionDispatch
'/:controller/*foo/bar' => %w{ controller foo },
}.each do |path, expected|
define_method(:"test_names_#{path}") do
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
path,
{ :controller => /.+/ },
["/", ".", "?"]
@@ -75,12 +75,8 @@ module ActionDispatch
end
end
- def test_to_raise_exception_with_bad_expression
- assert_raise(ArgumentError, "Bad expression: []") { Pattern.new [] }
- end
-
def test_to_regexp_with_extended_group
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/:name',
{ :name => /
#ROFL
@@ -101,13 +97,13 @@ module ActionDispatch
['/:foo(/:bar)', %w{ bar }],
['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }],
].each do |pattern, list|
- path = Pattern.new pattern
+ path = Pattern.from_string pattern
assert_equal list.sort, path.optional_names.sort
end
end
def test_to_regexp_match_non_optional
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/:name',
{ :name => /\d+/ },
["/", ".", "?"]
@@ -118,7 +114,7 @@ module ActionDispatch
end
def test_to_regexp_with_group
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/:name',
{ :name => /(tender|love)/ },
["/", ".", "?"]
@@ -131,7 +127,7 @@ module ActionDispatch
def test_ast_sets_regular_expressions
requirements = { :name => /(tender|love)/, :value => /./ }
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/:name/:value',
requirements,
["/", ".", "?"]
@@ -148,7 +144,7 @@ module ActionDispatch
end
def test_match_data_with_group
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/:name',
{ :name => /(tender|love)/ },
["/", ".", "?"]
@@ -160,7 +156,7 @@ module ActionDispatch
end
def test_match_data_with_multi_group
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/:name/:id',
{ :name => /t(((ender|love)))()/ },
["/", ".", "?"]
@@ -175,7 +171,7 @@ module ActionDispatch
def test_star_with_custom_re
z = /\d+/
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/*foo',
{ :foo => z },
["/", ".", "?"]
@@ -185,7 +181,7 @@ module ActionDispatch
end
def test_insensitive_regexp_with_group
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
'/page/:name/aaron',
{ :name => /(tender|love)/i },
["/", ".", "?"]
@@ -197,7 +193,7 @@ module ActionDispatch
end
def test_to_regexp_with_strexp
- strexp = Router::Strexp.new('/:controller', { }, ["/", ".", "?"])
+ strexp = Router::Strexp.build('/:controller', { }, ["/", ".", "?"])
path = Pattern.new strexp
x = %r{\A/([^/.?]+)\Z}
@@ -205,20 +201,20 @@ module ActionDispatch
end
def test_to_regexp_defaults
- path = Pattern.new '/:controller(/:action(/:id))'
+ path = Pattern.from_string '/:controller(/:action(/:id))'
expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}
assert_equal expected, path.to_regexp
end
def test_failed_match
- path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
uri = 'content'
assert_not path =~ uri
end
def test_match_controller
- path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
uri = '/content'
match = path =~ uri
@@ -230,7 +226,7 @@ module ActionDispatch
end
def test_match_controller_action
- path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
uri = '/content/list'
match = path =~ uri
@@ -242,7 +238,7 @@ module ActionDispatch
end
def test_match_controller_action_id
- path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
uri = '/content/list/10'
match = path =~ uri
@@ -254,7 +250,7 @@ module ActionDispatch
end
def test_match_literal
- path = Path::Pattern.new "/books(/:action(.:format))"
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
uri = '/books'
match = path =~ uri
@@ -264,7 +260,7 @@ module ActionDispatch
end
def test_match_literal_with_action
- path = Path::Pattern.new "/books(/:action(.:format))"
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
uri = '/books/list'
match = path =~ uri
@@ -274,7 +270,7 @@ module ActionDispatch
end
def test_match_literal_with_action_and_format
- path = Path::Pattern.new "/books(/:action(.:format))"
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
uri = '/books/list.rss'
match = path =~ uri
diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb
index cbe6284714..21d867aca0 100644
--- a/actionpack/test/journey/route_test.rb
+++ b/actionpack/test/journey/route_test.rb
@@ -5,7 +5,7 @@ module ActionDispatch
class TestRoute < ActiveSupport::TestCase
def test_initialize
app = Object.new
- path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))'
defaults = {}
route = Route.new("name", app, path, {}, defaults)
@@ -16,7 +16,7 @@ module ActionDispatch
def test_route_adds_itself_as_memo
app = Object.new
- path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))'
defaults = {}
route = Route.new("name", app, path, {}, defaults)
@@ -26,21 +26,21 @@ module ActionDispatch
end
def test_ip_address
- path = Path::Pattern.new '/messages/:id(.:format)'
+ path = Path::Pattern.from_string '/messages/:id(.:format)'
route = Route.new("name", nil, path, {:ip => '192.168.1.1'},
{ :controller => 'foo', :action => 'bar' })
assert_equal '192.168.1.1', route.ip
end
def test_default_ip
- path = Path::Pattern.new '/messages/:id(.:format)'
+ path = Path::Pattern.from_string '/messages/:id(.:format)'
route = Route.new("name", nil, path, {},
{ :controller => 'foo', :action => 'bar' })
assert_equal(//, route.ip)
end
def test_format_with_star
- path = Path::Pattern.new '/:controller/*extra'
+ path = Path::Pattern.from_string '/:controller/*extra'
route = Route.new("name", nil, path, {},
{ :controller => 'foo', :action => 'bar' })
assert_equal '/foo/himom', route.format({
@@ -50,7 +50,7 @@ module ActionDispatch
end
def test_connects_all_match
- path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))'
+ path = Path::Pattern.from_string '/:controller(/:action(/:id(.:format)))'
route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' })
assert_equal '/foo/bar/10', route.format({
@@ -61,21 +61,21 @@ module ActionDispatch
end
def test_extras_are_not_included_if_optional
- path = Path::Pattern.new '/page/:id(/:action)'
+ path = Path::Pattern.from_string '/page/:id(/:action)'
route = Route.new("name", nil, path, { }, { :action => 'show' })
assert_equal '/page/10', route.format({ :id => 10 })
end
def test_extras_are_not_included_if_optional_with_parameter
- path = Path::Pattern.new '(/sections/:section)/pages/:id'
+ path = Path::Pattern.from_string '(/sections/:section)/pages/:id'
route = Route.new("name", nil, path, { }, { :action => 'show' })
assert_equal '/pages/10', route.format({:id => 10})
end
def test_extras_are_not_included_if_optional_parameter_is_nil
- path = Path::Pattern.new '(/sections/:section)/pages/:id'
+ path = Path::Pattern.from_string '(/sections/:section)/pages/:id'
route = Route.new("name", nil, path, { }, { :action => 'show' })
assert_equal '/pages/10', route.format({:id => 10, :section => nil})
@@ -85,10 +85,10 @@ module ActionDispatch
constraints = {:required_defaults => [:controller, :action]}
defaults = {:controller=>"pages", :action=>"show"}
- path = Path::Pattern.new "/page/:id(/:action)(.:format)"
+ path = Path::Pattern.from_string "/page/:id(/:action)(.:format)"
specific = Route.new "name", nil, path, constraints, defaults
- path = Path::Pattern.new "/:controller(/:action(/:id))(.:format)"
+ path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)"
generic = Route.new "name", nil, path, constraints
knowledge = {:id=>20, :controller=>"pages", :action=>"show"}
diff --git a/actionpack/test/journey/router/strexp_test.rb b/actionpack/test/journey/router/strexp_test.rb
deleted file mode 100644
index 7ccdfb7b4d..0000000000
--- a/actionpack/test/journey/router/strexp_test.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'abstract_unit'
-
-module ActionDispatch
- module Journey
- class Router
- class TestStrexp < ActiveSupport::TestCase
- def test_many_names
- exp = Strexp.new(
- "/:controller(/:action(/:id(.:format)))",
- {:controller=>/.+?/},
- ["/", ".", "?"],
- true)
-
- assert_equal ["controller", "action", "id", "format"], exp.names
- end
-
- def test_names
- {
- "/bar(.:format)" => %w{ format },
- ":format" => %w{ format },
- ":format-" => %w{ format },
- ":format0" => %w{ format0 },
- ":format1,:format2" => %w{ format1 format2 },
- }.each do |string, expected|
- exp = Strexp.new(string, {}, ["/", ".", "?"])
- assert_equal expected, exp.names
- end
- end
- end
- end
- end
-end
diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb
index 93348f4647..9b2b85ec73 100644
--- a/actionpack/test/journey/router/utils_test.rb
+++ b/actionpack/test/journey/router/utils_test.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'abstract_unit'
module ActionDispatch
@@ -5,17 +6,25 @@ 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_uri_unescape_with_utf8_string
+ assert_equal "Šašinková", Utils.unescape_uri("%C5%A0a%C5%A1inkov%C3%A1".force_encoding(Encoding::US_ASCII))
+ end
+
def test_normalize_path_not_greedy
assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz")
end
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index a286f77633..2e7e8e1bea 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -4,24 +4,15 @@ 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
+ @app = Routing::RouteSet::Dispatcher.new({})
@routes = Routes.new
- @router = Router.new(@routes, {})
+ @router = Router.new(@routes)
@formatter = Formatter.new(@routes)
end
- def test_request_class_reader
- klass = Object.new
- router = Router.new(routes, :request_class => klass)
- assert_equal klass, router.request_class
- end
-
class FakeRequestFeeler < Struct.new(:env, :called)
def new env
self.env = env
@@ -39,33 +30,33 @@ module ActionDispatch
end
def test_dashes
- router = Router.new(routes, {})
+ router = Router.new(routes)
- exp = Router::Strexp.new '/foo-bar-baz', {}, ['/.?']
+ exp = Router::Strexp.build '/foo-bar-baz', {}, ['/.?']
path = Path::Pattern.new exp
routes.add_route nil, path, {}, {:id => nil}, {}
env = rails_env 'PATH_INFO' => '/foo-bar-baz'
called = false
- router.recognize(env) do |r, _, params|
+ router.recognize(env) do |r, params|
called = true
end
assert called
end
def test_unicode
- router = Router.new(routes, {})
+ router = Router.new(routes)
#match the escaped version of /ほげ
- exp = Router::Strexp.new '/%E3%81%BB%E3%81%92', {}, ['/.?']
+ exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?']
path = Path::Pattern.new exp
routes.add_route nil, path, {}, {:id => nil}, {}
env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92'
called = false
- router.recognize(env) do |r, _, params|
+ router.recognize(env) do |r, params|
called = true
end
assert called
@@ -73,17 +64,17 @@ module ActionDispatch
def test_request_class_and_requirements_success
klass = FakeRequestFeeler.new nil
- router = Router.new(routes, {:request_class => klass })
+ router = Router.new(routes)
requirements = { :hello => /world/ }
- exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?']
+ exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
path = Path::Pattern.new exp
routes.add_route nil, path, requirements, {:id => nil}, {}
- env = rails_env 'PATH_INFO' => '/foo/10'
- router.recognize(env) do |r, _, params|
+ env = rails_env({'PATH_INFO' => '/foo/10'}, klass)
+ router.recognize(env) do |r, params|
assert_equal({:id => '10'}, params)
end
@@ -93,17 +84,17 @@ module ActionDispatch
def test_request_class_and_requirements_fail
klass = FakeRequestFeeler.new nil
- router = Router.new(routes, {:request_class => klass })
+ router = Router.new(routes)
requirements = { :hello => /mom/ }
- exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?']
+ exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
path = Path::Pattern.new exp
router.routes.add_route nil, path, requirements, {:id => nil}, {}
- env = rails_env 'PATH_INFO' => '/foo/10'
- router.recognize(env) do |r, _, params|
+ env = rails_env({'PATH_INFO' => '/foo/10'}, klass)
+ router.recognize(env) do |r, params|
flunk 'route should not be found'
end
@@ -111,24 +102,29 @@ module ActionDispatch
assert_equal env.env, klass.env
end
- class CustomPathRequest < Router::NullReq
+ class CustomPathRequest < ActionDispatch::Request
def path_info
env['custom.path_info']
end
+
+ def path_info=(x)
+ env['custom.path_info'] = x
+ end
end
def test_request_class_overrides_path_info
- router = Router.new(routes, {:request_class => CustomPathRequest })
+ router = Router.new(routes)
- exp = Router::Strexp.new '/bar', {}, ['/.?']
+ exp = Router::Strexp.build '/bar', {}, ['/.?']
path = Path::Pattern.new exp
routes.add_route nil, path, {}, {}, {}
- env = rails_env 'PATH_INFO' => '/foo', 'custom.path_info' => '/bar'
+ env = rails_env({'PATH_INFO' => '/foo',
+ 'custom.path_info' => '/bar'}, CustomPathRequest)
recognized = false
- router.recognize(env) do |r, _, params|
+ router.recognize(env) do |r, params|
recognized = true
end
@@ -137,14 +133,14 @@ module ActionDispatch
def test_regexp_first_precedence
add_routes @router, [
- Router::Strexp.new("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']),
- Router::Strexp.new("/whois/:id(.:format)", {}, ['/', '.', '?'])
+ Router::Strexp.build("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']),
+ Router::Strexp.build("/whois/:id(.:format)", {}, ['/', '.', '?'])
]
env = rails_env 'PATH_INFO' => '/whois/example.com'
list = []
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
list << r
end
assert_equal 2, list.length
@@ -156,50 +152,50 @@ module ActionDispatch
def test_required_parts_verified_are_anchored
add_routes @router, [
- Router::Strexp.new("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false)
+ Router::Strexp.build("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false)
]
assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(:path_info, nil, { :id => '10' }, { })
+ @formatter.generate(nil, { :id => '10' }, { })
end
end
def test_required_parts_are_verified_when_building
add_routes @router, [
- Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
+ Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
]
- path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { })
+ path, _ = @formatter.generate(nil, { :id => '10' }, { })
assert_equal '/foo/10', path
assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(:path_info, nil, { :id => 'aa' }, { })
+ @formatter.generate(nil, { :id => 'aa' }, { })
end
end
def test_only_required_parts_are_verified
add_routes @router, [
- Router::Strexp.new("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false)
+ Router::Strexp.build("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false)
]
- path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { })
+ path, _ = @formatter.generate(nil, { :id => '10' }, { })
assert_equal '/foo/10', path
- path, _ = @formatter.generate(:path_info, nil, { }, { })
+ path, _ = @formatter.generate(nil, { }, { })
assert_equal '/foo', path
- path, _ = @formatter.generate(:path_info, nil, { :id => 'aa' }, { })
+ path, _ = @formatter.generate(nil, { :id => 'aa' }, { })
assert_equal '/foo/aa', path
end
def test_knows_what_parts_are_missing_from_named_route
route_name = "gorby_thunderhorse"
- pattern = Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
+ pattern = Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
path = Path::Pattern.new pattern
@router.routes.add_route nil, path, {}, {}, route_name
error = assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(:path_info, route_name, { }, { })
+ @formatter.generate(route_name, { }, { })
end
assert_match(/missing required keys: \[:id\]/, error.message)
@@ -207,42 +203,43 @@ module ActionDispatch
def test_X_Cascade
add_routes @router, [ "/messages(.:format)" ]
- resp = @router.call({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' })
+ resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }))
assert_equal ['Not Found'], resp.last
assert_equal 'pass', resp[1]['X-Cascade']
assert_equal 404, resp.first
end
def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes
- strexp = Router::Strexp.new("/", {}, ['/', '.', '?'], false)
- path = Path::Pattern.new strexp
+ route_set = Routing::RouteSet.new
+ mapper = Routing::Mapper.new route_set
+
app = lambda { |env| [200, {}, ['success!']] }
- @router.routes.add_route(app, path, {}, {}, {})
+ mapper.get '/weblog', :to => app
env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog')
- resp = @router.call(env)
+ resp = route_set.call env
assert_equal ['success!'], resp.last
assert_equal '', env['SCRIPT_NAME']
end
def test_defaults_merge_correctly
- path = Path::Pattern.new '/foo(/:id)'
+ path = Path::Pattern.from_string '/foo(/:id)'
@router.routes.add_route nil, path, {}, {:id => nil}, {}
env = rails_env 'PATH_INFO' => '/foo/10'
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal({:id => '10'}, params)
end
env = rails_env 'PATH_INFO' => '/foo'
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal({:id => nil}, params)
end
end
def test_recognize_with_unbound_regexp
add_routes @router, [
- Router::Strexp.new("/foo", { }, ['/', '.', '?'], false)
+ Router::Strexp.build("/foo", { }, ['/', '.', '?'], false)
]
env = rails_env 'PATH_INFO' => '/foo/bar'
@@ -255,7 +252,7 @@ module ActionDispatch
def test_bound_regexp_keeps_path_info
add_routes @router, [
- Router::Strexp.new("/foo", { }, ['/', '.', '?'], true)
+ Router::Strexp.build("/foo", { }, ['/', '.', '?'], true)
]
env = rails_env 'PATH_INFO' => '/foo'
@@ -287,14 +284,14 @@ module ActionDispatch
def test_required_part_in_recall
add_routes @router, [ "/messages/:a/:b" ]
- path, _ = @formatter.generate(:path_info, nil, { :a => 'a' }, { :b => 'b' })
+ path, _ = @formatter.generate(nil, { :a => 'a' }, { :b => 'b' })
assert_equal "/messages/a/b", path
end
def test_splat_in_recall
add_routes @router, [ "/*path" ]
- path, _ = @formatter.generate(:path_info, nil, { }, { :path => 'b' })
+ path, _ = @formatter.generate(nil, { }, { :path => 'b' })
assert_equal "/b", path
end
@@ -304,35 +301,35 @@ module ActionDispatch
"/messages/:id(.:format)"
]
- path, _ = @formatter.generate(:path_info, nil, { :id => 10 }, { :action => 'index' })
+ path, _ = @formatter.generate(nil, { :id => 10 }, { :action => 'index' })
assert_equal "/messages/index/10", path
end
def test_nil_path_parts_are_ignored
- path = Path::Pattern.new "/:controller(/:action(.:format))"
+ path = Path::Pattern.from_string "/:controller(/:action(.:format))"
@router.routes.add_route @app, path, {}, {}, {}
params = { :controller => "tasks", :format => nil }
extras = { :action => 'lol' }
- path, _ = @formatter.generate(:path_info, nil, params, extras)
+ path, _ = @formatter.generate(nil, params, extras)
assert_equal '/tasks', path
end
def test_generate_slash
params = [ [:controller, "tasks"],
[:action, "show"] ]
- str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true)
+ str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true)
path = Path::Pattern.new str
@router.routes.add_route @app, path, {}, {}, {}
- path, _ = @formatter.generate(:path_info, nil, Hash[params], {})
+ path, _ = @formatter.generate(nil, Hash[params], {})
assert_equal '/', path
end
def test_generate_calls_param_proc
- path = Path::Pattern.new '/:controller(/:action)'
+ path = Path::Pattern.from_string '/:controller(/:action)'
@router.routes.add_route @app, path, {}, {}, {}
parameterized = []
@@ -340,7 +337,6 @@ module ActionDispatch
[:action, "show"] ]
@formatter.generate(
- :path_info,
nil,
Hash[params],
{},
@@ -350,31 +346,42 @@ module ActionDispatch
end
def test_generate_id
- path = Path::Pattern.new '/:controller(/:action)'
+ path = Path::Pattern.from_string '/:controller(/:action)'
@router.routes.add_route @app, path, {}, {}, {}
path, params = @formatter.generate(
- :path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {})
+ nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {})
assert_equal '/tasks/show', path
assert_equal({:id => 1}, params)
end
def test_generate_escapes
- path = Path::Pattern.new '/:controller(/:action)'
+ path = Path::Pattern.from_string '/:controller(/:action)'
@router.routes.add_route @app, path, {}, {}, {}
- path, _ = @formatter.generate(:path_info,
- nil, { :controller => "tasks",
+ path, _ = @formatter.generate(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.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(
+ 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)'
+ path = Path::Pattern.from_string '/:controller(/:action)'
@router.routes.add_route @app, path, {}, {}, {}
- path, params = @formatter.generate(:path_info,
+ path, params = @formatter.generate(
nil, { :id => 1,
:controller => "tasks",
:action => "show",
@@ -385,10 +392,10 @@ module ActionDispatch
end
def test_generate_uses_recall_if_needed
- path = Path::Pattern.new '/:controller(/:action(/:id))'
+ path = Path::Pattern.from_string '/:controller(/:action(/:id))'
@router.routes.add_route @app, path, {}, {}, {}
- path, params = @formatter.generate(:path_info,
+ path, params = @formatter.generate(
nil,
{:controller =>"tasks", :id => 10},
{:action =>"index"})
@@ -397,10 +404,10 @@ module ActionDispatch
end
def test_generate_with_name
- path = Path::Pattern.new '/:controller(/:action)'
+ path = Path::Pattern.from_string '/:controller(/:action)'
@router.routes.add_route @app, path, {}, {}, {}
- path, params = @formatter.generate(:path_info,
+ path, params = @formatter.generate(
"tasks",
{:controller=>"tasks"},
{:controller=>"tasks", :action=>"index"})
@@ -414,14 +421,14 @@ module ActionDispatch
'/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" },
}.each do |request_path, expected|
define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do
- path = Path::Pattern.new "/:controller(/:action(/:id))"
+ path = Path::Pattern.from_string "/:controller(/:action(/:id))"
app = Object.new
route = @router.routes.add_route(app, path, {}, {}, {})
env = rails_env 'PATH_INFO' => request_path
called = false
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -436,14 +443,14 @@ module ActionDispatch
:splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }]
}.each do |name, (request_path, expected)|
define_method("test_recognize_#{name}") do
- path = Path::Pattern.new '/:segment/*splat'
+ path = Path::Pattern.from_string '/:segment/*splat'
app = Object.new
route = @router.routes.add_route(app, path, {}, {}, {})
env = rails_env 'PATH_INFO' => request_path
called = false
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -454,7 +461,7 @@ module ActionDispatch
end
def test_namespaced_controller
- strexp = Router::Strexp.new(
+ strexp = Router::Strexp.build(
"/:controller(/:action(/:id))",
{ :controller => /.+?/ },
["/", ".", "?"]
@@ -471,7 +478,7 @@ module ActionDispatch
:id => '10'
}
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -480,14 +487,14 @@ module ActionDispatch
end
def test_recognize_literal
- path = Path::Pattern.new "/books(/:action(.:format))"
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
app = Object.new
route = @router.routes.add_route(app, path, {}, {:controller => 'books'})
env = rails_env 'PATH_INFO' => '/books/list.rss'
expected = { :controller => 'books', :action => 'list', :format => 'rss' }
called = false
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -497,7 +504,7 @@ module ActionDispatch
end
def test_recognize_head_request_as_get_route
- path = Path::Pattern.new "/books(/:action(.:format))"
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
app = Object.new
conditions = {
:request_method => 'GET'
@@ -508,7 +515,7 @@ module ActionDispatch
"REQUEST_METHOD" => "HEAD"
called = false
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
called = true
end
@@ -516,7 +523,7 @@ module ActionDispatch
end
def test_recognize_cares_about_verbs
- path = Path::Pattern.new "/books(/:action(.:format))"
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
app = Object.new
conditions = {
:request_method => 'GET'
@@ -532,7 +539,7 @@ module ActionDispatch
"REQUEST_METHOD" => "POST"
called = false
- @router.recognize(env) do |r, _, params|
+ @router.recognize(env) do |r, params|
assert_equal post, r
called = true
end
@@ -544,15 +551,17 @@ module ActionDispatch
def add_routes router, paths
paths.each do |path|
- path = Path::Pattern.new path
+ if String === path
+ path = Path::Pattern.from_string path
+ else
+ path = Path::Pattern.new path
+ end
router.routes.add_route @app, path, {}, {}, {}
end
end
- RailsEnv = Struct.new(:env)
-
- def rails_env env
- RailsEnv.new rack_env env
+ def rails_env env, klass = ActionDispatch::Request
+ klass.new env
end
def rack_env env
diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb
index 25e0321d31..a4efc82b8c 100644
--- a/actionpack/test/journey/routes_test.rb
+++ b/actionpack/test/journey/routes_test.rb
@@ -5,7 +5,7 @@ module ActionDispatch
class TestRoutes < ActiveSupport::TestCase
def test_clear
routes = Routes.new
- exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?']
+ exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
path = Path::Pattern.new exp
requirements = { :hello => /world/ }
@@ -18,7 +18,7 @@ module ActionDispatch
def test_ast
routes = Routes.new
- path = Path::Pattern.new '/hello'
+ path = Path::Pattern.from_string '/hello'
routes.add_route nil, path, {}, {}, {}
ast = routes.ast
@@ -28,7 +28,7 @@ module ActionDispatch
def test_simulator_changes
routes = Routes.new
- path = Path::Pattern.new '/hello'
+ path = Path::Pattern.from_string '/hello'
routes.add_route nil, path, {}, {}, {}
sim = routes.simulator
@@ -40,8 +40,8 @@ module ActionDispatch
#def add_route app, path, conditions, defaults, name = nil
routes = Routes.new
- one = Path::Pattern.new '/hello'
- two = Path::Pattern.new '/aaron'
+ one = Path::Pattern.from_string '/hello'
+ two = Path::Pattern.from_string '/aaron'
routes.add_route nil, one, {}, {}, 'aaron'
routes.add_route nil, two, {}, {}, 'aaron'
diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb
index 0028aaa629..09ca7ff73b 100644
--- a/actionpack/test/routing/helper_test.rb
+++ b/actionpack/test/routing/helper_test.rb
@@ -26,6 +26,20 @@ module ActionDispatch
x.new.pond_duck_path Duck.new
end
end
+
+ def test_path_deprecation
+ rs = ::ActionDispatch::Routing::RouteSet.new
+ rs.draw do
+ resources :ducks
+ end
+
+ x = Class.new {
+ include rs.url_helpers(false)
+ }
+ assert_deprecated do
+ assert_equal '/ducks', x.new.ducks_path
+ end
+ end
end
end
end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index a0f298a6b1..3fc2ab178c 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,318 +1,159 @@
-* Date select helpers accept a format string for the months selector via the
- new option `:month_format_string`.
+* Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree.
- When rendered, the format string gets passed keys `:number` (integer), and
- `:name` (string), in order to be able to interpolate them as in
+ *DHH*
- '%{name} (%<number>02d)'
+* Add `PartialIteration` object used when rendering collections.
- for example.
+ The iteration object is available as the local variable
+ `#{template_name}_iteration` when rendering partials with collections.
- This option is motivated by #13618.
+ It gives access to the `size` of the collection being iterated over,
+ the current `index` and two convenience methods `first?` and `last?`.
- *Xavier Noria*
+ *Joel Junström*, *Lucas Uyezu*
-* Added `config.action_view.raise_on_missing_translations` to define whether an
- error should be raised for missing translations.
+* Return an absolute instead of relative path from an asset url in the case
+ of the `asset_host` proc returning nil
- Fixes #13196.
+ *Jolyon Pawlyn*
- *Kassio Borges*
+* Fix `html_escape_once` to properly handle hex escape sequences (e.g. &#x1a2b;)
-* Improved ERB dependency detection. New argument types and formattings for the `render`
- calls can be matched.
+ *John F. Douthat*
- Fixes #13074, #13116.
+* Added String support for min and max properties for date field helpers.
- *João Britto*
+ *Todd Bealmear*
-* Use `display:none` instead of `display:inline` for hidden fields.
+* The `highlight` helper now accepts a block to be used instead of the `highlighter`
+ option.
- Fixes #6403.
+ *Lucas Mazza*
- *Gaelian Ditchburn*
+* The `except` and `highlight` helpers now accept regular expressions.
-* The `video_tag` helper accepts a number as `:size`.
+ *Jan Szumiec*
- The `:size` option of the `video_tag` helper now can be specified
- with a stringified number. The `width` and `height` attributes of
- the generated tag will be the same.
+* Flatten the array parameter in `safe_join`, so it behaves consistently with
+ `Array#join`.
- *Kuldeep Aggarwal*
+ *Paul Grayson*
-* A Cycle object should accept an array and cycle through it as it would with a set of
- comma-separated objects.
+* Honor `html_safe` on array elements in tag values, as we do for plain string
+ values.
- arr = [1,2,3]
- cycle(arr) # => '1'
- cycle(arr) # => '2'
- cycle(arr) # => '3'
+ *Paul Grayson*
- Previously, it would return the array as a string, because it took the array as a
- single object:
+* Add `ActionView::Template::Handler.unregister_template_handler`.
- arr = [1,2,3]
- cycle(arr) # => '[1,2,3]'
- cycle(arr) # => '[1,2,3]'
- cycle(arr) # => '[1,2,3]'
+ It performs the opposite of `ActionView::Template::Handler.register_template_handler`.
- *Kristian Freeman*
+ *Zuhao Wan*
-* Label tags generated by collection helpers only inherit the `:index` and
- `:namespace` from the input, because only these attributes modifies the
- `for` attribute of the label. Also, the input attributes don't have
- precedence over the label attributes anymore.
+* Bring `cache_digest` rake tasks up-to-date with the latest API changes
- Before:
-
- collection = [[1, true, { class: 'foo' }]]
- f.collection_check_boxes :options, collection, :second, :first do |b|
- b.label(class: 'my_custom_class')
- end
-
- # => <label class="foo" for="user_active_true">1</label>
-
- After:
-
- collection = [[1, true, { class: 'foo' }]]
- f.collection_check_boxes :options, collection, :second, :first do |b|
- b.label(class: 'my_custom_class')
- end
-
- # => <label class="my_custom_class" for="user_active_true">1</label>
-
- *Andriel Nuernberg*
-
-* Fixed a long-standing bug in `json_escape` that causes quotation marks to be stripped.
- This method also escapes the \u2028 and \u2029 unicode newline characters which are
- treated as \n in JavaScript. This matches the behaviour of the AS::JSON encoder. (The
- original change in the encoder was introduced in #10534.)
-
- *Godfrey Chan*
-
-* `ActionView::MissingTemplate` includes underscore when raised for a partial.
-
- Fixes #13002.
-
- *Yves Senn*
-
-* Use `set_backtrace` instead of instance variable `@backtrace` in ActionView exceptions.
-
- *Shimpei Makimoto*
-
-* Fix `simple_format` escapes own output when passing `sanitize: true`.
-
- *Paul Seidemann*
-
-* Ensure `ActionView::Digestor.cache` is correctly cleaned up when
- combining recursive templates with `ActionView::Resolver.caching = false`.
+ *Jiri Pospisil*
- *wyaeld*
+* Allow custom `:host` option to be passed to `asset_url` helper that
+ overwrites `config.action_controller.asset_host` for particular asset.
-* Fix `collection_check_boxes` generated hidden input to use the name attribute provided
- in the options hash.
+ *Hubert Łępicki*
- *Angel N. Sciortino*
+* Deprecate `AbstractController::Base.parent_prefixes`.
+ Override `AbstractController::Base.local_prefixes` when you want to change
+ where to find views.
-* Fix some edge cases for AV `select` helper with `:selected` option.
+ *Nick Sutterer*
- *Bogdan Gusiev*
+* Take label values into account when doing I18n lookups for model attributes.
-* Ability to pass a block to the `select` helper.
+ The following:
- Example:
+ # form.html.erb
+ <%= form_for @post do |f| %>
+ <%= f.label :type, value: "long" %>
+ <% end %>
- <%= 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 -%>
+ # en.yml
+ en:
+ activerecord:
+ attributes:
+ post/long: "Long-form Post"
- *Bogdan Gusiev*
+ Used to simply return "long", but now it will return "Long-form
+ Post".
-* Handle `:namespace` form option in collection labels.
+ *Joshua Cody*
- *Vasiliy Ermolovich*
-
-* Fix `form_for` when both `namespace` and `as` options are present.
-
- `as` option no longer overwrites `namespace` option when generating
- html id attribute of the form element.
-
- *Adam Niedzielski*
-
-* Fix `excerpt` when `:separator` is `nil`.
-
- *Paul Nikitochkin*
-
-* Only cache template digests if `config.cache_template_loading` is true.
-
- *Josh Lauer*, *Justin Ridgewell*
-
-* Fixed a bug where the lookup details were not being taken into account
- when caching the digest of a template - changes to the details now
- cause a different cache key to be used.
-
- *Daniel Schierbeck*
-
-* Added an `extname` hash option for `javascript_include_tag` method.
+* Change `asset_path` to use File.join to create proper paths:
Before:
- javascript_include_tag('templates.jst')
- # => <script src="/javascripts/templates.jst.js"></script>
+ https://some.host.com//assets/some.js
After:
- javascript_include_tag('templates.jst', extname: false )
- # => <script src="/javascripts/templates.jst"></script>
-
- *Nathan Stitt*
-
-* Fix `current_page?` when the URL contains escaped characters and the
- original URL is using the hexadecimal lowercased.
+ https://some.host.com/assets/some.js
- *Rafael Mendonça França*
+ *Peter Schröder*
-* Fix `text_area` to behave like `text_field` when `nil` is given as
- value.
+* Change `favicon_link_tag` default mimetype from `image/vnd.microsoft.icon` to
+ `image/x-icon`.
Before:
- f.text_field :field, value: nil #=> <input value="">
- f.text_area :field, value: nil #=> <textarea>value of field</textarea>
+ #=> favicon_link_tag 'myicon.ico'
+ <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
After:
- f.text_area :field, value: nil #=> <textarea></textarea>
+ #=> favicon_link_tag 'myicon.ico'
+ <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
- *Joel Cogen*
+ *Geoffroy Lorieux*
-* Element of the `grouped_options_for_select` can
- optionally contain html attributes as the last element of the array.
+* Remove wrapping div with inline styles for hidden form fields.
- grouped_options_for_select(
- [["North America", [['United States','US'],"Canada"], data: { foo: 'bar' }]]
- )
-
- *Vasiliy Ermolovich*
+ We are dropping HTML 4.01 and XHTML strict compliance since input tags directly
+ inside a form are valid HTML5, and the absence of inline styles help in validating
+ for Content Security Policy.
-* Fix default rendered format problem when calling `render` without :content_type option.
- It should return :html. Fix #11393.
+ *Joost Baaij*
- *Gleb Mazovetskiy*, *Oleg*, *kennyj*
+* `collection_check_boxes` respects `:index` option for the hidden filed name.
-* Fix `link_to` with block and url hashes.
+ Fixes #14147.
- Before:
-
- link_to(action: 'bar', controller: 'foo') { content_tag(:span, 'Example site') }
- # => "<a action=\"bar\" controller=\"foo\"><span>Example site</span></a>"
-
- After:
-
- link_to(action: 'bar', controller: 'foo') { content_tag(:span, 'Example site') }
- # => "<a href=\"/foo/bar\"><span>Example site</span></a>"
-
- *Murahashi Sanemat Kenichi*
-
-* Fix "Stack Level Too Deep" error when redering recursive partials.
-
- Fixes #11340.
-
- *Rafael Mendonça França*
-
-* Added an `enforce_utf8` hash option for `form_tag` method.
-
- Control to output a hidden input tag with name `utf8` without monkey
- patching.
-
- Before:
-
- form_tag
- # => '<form>..<input name="utf8" type="hidden" value="&#x2713;" />..</form>'
-
- After:
-
- form_tag
- # => '<form>..<input name="utf8" type="hidden" value="&#x2713;" />..</form>'
-
- form_tag({}, { :enforce_utf8 => false })
- # => '<form>....</form>'
-
- *ma2gedev*
-
-* Remove the deprecated `include_seconds` argument from `distance_of_time_in_words`,
- pass in an `:include_seconds` hash option to use this feature.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated block passing to `FormBuilder#new`.
-
- *Vipul A M*
-
-* Pick `DateField` `DateTimeField` and `ColorField` values from stringified options allowing use of symbol keys with helpers.
-
- *Jon Rowe*
-
-* Remove the deprecated `prompt` argument from `grouped_options_for_select`,
- pass in a `:prompt` hash option to use this feature.
-
- *kennyj*
-
-* Always escape the result of `link_to_unless` method.
-
- Before:
-
- link_to_unless(true, '<b>Showing</b>', 'github.com')
- # => "<b>Showing</b>"
-
- After:
-
- link_to_unless(true, '<b>Showing</b>', 'github.com')
- # => "&lt;b&gt;Showing&lt;/b&gt;"
-
- *dtaniwaki*
-
-* Use a case insensitive URI Regexp for #asset_path.
-
- This fix a problem where the same asset path using different case are generating
- different URIs.
+ *Vasiliy Ermolovich*
- Before:
+* `date_select` helper with option `with_css_classes: true` does not overwrite other classes.
- 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\" />"
+ *Izumi Wong-Horiuchi*
- After:
+* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY`
+ as input.
- image_tag("HTTP://google.com")
- # => "<img alt=\"Google\" src=\"HTTP://google.com\" />"
- image_tag("http://google.com")
- # => "<img alt=\"Google\" src=\"http://google.com\" />"
+ Fixes #14405.
- *David Celis*
+ *Yves Senn*
-* Element of the `collection_check_boxes` and `collection_radio_buttons` can
- optionally contain html attributes as the last element of the array.
+* 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.
- *Alex Peattie*
+ Fixes #14254.
-* Fix an issue where partials with a number in the filename weren't being digested for cache dependencies.
+ *Sergey Prikhodko*
- *Bryan Ricker*
+* Take variants into account when calculating template digests in ActionView::Digestor.
-* First release, ActionView extracted from ActionPack
+ 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.
- *Piotr Sarnacki*, *Łukasz Strzałkowski*
+ *Piotr Chmolowski, Ł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/README.rdoc b/actionview/README.rdoc
index 35f805346c..5bb62c7562 100644
--- a/actionview/README.rdoc
+++ b/actionview/README.rdoc
@@ -29,6 +29,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc
index 104b3e288d..6c4e5e983a 100644
--- a/actionview/RUNNING_UNIT_TESTS.rdoc
+++ b/actionview/RUNNING_UNIT_TESTS.rdoc
@@ -4,7 +4,7 @@ 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"
-Rake can be found at http://rake.rubyforge.org
+Rake can be found at http://docs.seattlerb.org/rake/.
== Running by hand
@@ -19,8 +19,8 @@ which can be further narrowed down to one test:
== Dependency on Active Record and database setup
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,
+activerecord and sqlite3 installed. If Active Record is not in
+actionview/../activerecord directory, or the sqlite3 rubygem is not installed,
these tests are skipped.
Other tests are runnable from a fresh copy of actionview without any configuration.
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 455ce531ae..86c55ffb51 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -66,15 +66,6 @@ module ActionView #:nodoc:
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
- # If you need to find out whether a certain local variable has been assigned a value in a particular render call,
- # you need to use the following pattern:
- #
- # <% if local_assigns.has_key? :headline %>
- # Headline: <%= headline %>
- # <% end %>
- #
- # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction.
- #
# === Template caching
#
# By default, Rails will compile each template to a method in order to render it. When you alter a template,
@@ -131,7 +122,8 @@ module ActionView #:nodoc:
# end
# end
#
- # More builder documentation can be found at http://builder.rubyforge.org.
+ # For more information on Builder please consult the [source
+ # code](https://github.com/jimweirich/builder).
class Base
include Helpers, ::ERB::Util, Context
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index 0ccf2515c5..e34bdd4a46 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -53,6 +53,12 @@ module ActionView
\s* # followed by optional spaces
/x
+ # Part of any hash containing the :layout key
+ LAYOUT_HASH_KEY = /
+ (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
+ \s* # followed by optional spaces
+ /x
+
# Matches:
# partial: "comments/comment", collection: @all_comments => "comments/comment"
# (object: @single_comment, partial: "comments/comment") => "comments/comment"
@@ -65,9 +71,9 @@ module ActionView
# 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
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
+ (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
def self.call(name, template)
@@ -85,8 +91,8 @@ module ActionView
attr_reader :name, :template
private :name, :template
- private
+ private
def source
template.source
end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 5570e2a8dc..1f103786cb 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -9,54 +9,61 @@ module ActionView
@@digest_monitor = Monitor.new
class << self
- def digest(name, format, finder, options = {})
- details_key = finder.details_key.hash
- dependencies = Array.wrap(options[:dependencies])
- cache_key = ([name, details_key, format] + dependencies).join('.')
+ # 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, name, format, finder, options)
+ compute_and_store_digest(cache_key, options)
end
end
end
private
- def compute_and_store_digest(cache_key, name, format, finder, options) # called under @@digest_monitor lock
- klass = if options[:partial] || 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
+ 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(name, format, finder, 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
+ 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 :debug, " 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
@@ -68,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
@@ -88,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
@@ -97,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/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index aa49f1edc1..669050e7a7 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -7,7 +7,7 @@ module ActionView
# = Action View Asset Tag Helpers
module Helpers #:nodoc:
# This module provides methods for generating HTML that links views to assets such
- # as images, javascripts, stylesheets, and feeds. These methods do not verify
+ # as images, JavaScripts, stylesheets, and feeds. These methods do not verify
# the assets exist before linking to them:
#
# image_tag("rails.png")
@@ -142,22 +142,29 @@ 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
+ # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
#
# favicon_link_tag 'myicon.ico'
- # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
+ # # => <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 iPod Touch, iPhone, or iPad.
+ # 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'
@@ -165,7 +172,7 @@ module ActionView
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
@@ -251,19 +258,19 @@ module ActionView
# ==== Examples
#
# video_tag("trailer")
- # # => <video src="/videos/trailer" />
+ # # => <video src="/videos/trailer"></video>
# video_tag("trailer.ogg")
- # # => <video src="/videos/trailer.ogg" />
+ # # => <video src="/videos/trailer.ogg"></video>
# video_tag("trailer.ogg", controls: true, autobuffer: true)
- # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" />
+ # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video>
# video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
- # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" />
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
# video_tag("/trailers/hd.avi", size: "16x16")
- # # => <video src="/trailers/hd.avi" width="16" height="16" />
+ # # => <video src="/trailers/hd.avi" width="16" height="16"></video>
# video_tag("/trailers/hd.avi", size: "16")
- # # => <video height="16" src="/trailers/hd.avi" width="16" />
+ # # => <video height="16" src="/trailers/hd.avi" width="16"></video>
# video_tag("/trailers/hd.avi", height: '32', width: '32')
- # # => <video height="32" src="/trailers/hd.avi" width="32" />
+ # # => <video height="32" src="/trailers/hd.avi" width="32"></video>
# video_tag("trailer.ogg", "trailer.flv")
# # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
# video_tag(["trailer.ogg", "trailer.flv"])
@@ -282,11 +289,11 @@ module ActionView
# your public audios directory.
#
# audio_tag("sound")
- # # => <audio src="/audios/sound" />
+ # # => <audio src="/audios/sound"></audio>
# audio_tag("sound.wav")
- # # => <audio src="/audios/sound.wav" />
+ # # => <audio src="/audios/sound.wav"></audio>
# audio_tag("sound.wav", autoplay: true, controls: true)
- # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" />
+ # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
# audio_tag("sound.wav", "sound.mid")
# # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
def audio_tag(*sources)
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index c830ab23e3..9e8d005ec7 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -88,9 +88,12 @@ module ActionView
# still sending assets for plain HTTP requests from asset hosts. If you don't
# have SSL certificates for each of the asset hosts this technique allows you
# to avoid warnings in the client about mixed media.
+ # Note that the request parameter might not be supplied, e.g. when the assets
+ # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda,
+ # since a Proc allows missing parameters and sets them to nil.
#
# config.action_controller.asset_host = Proc.new { |source, request|
- # if request.ssl?
+ # if request && request.ssl?
# "#{request.protocol}#{request.host_with_port}"
# else
# "#{request.protocol}assets.example.com"
@@ -113,13 +116,13 @@ module ActionView
#
# All other asset *_path helpers delegate through this method.
#
- # asset_path "application.js" # => /application.js
- # asset_path "application", type: :javascript # => /javascripts/application.js
- # asset_path "application", type: :stylesheet # => /stylesheets/application.css
+ # asset_path "application.js" # => /assets/application.js
+ # asset_path "application", type: :javascript # => /assets/application.js
+ # asset_path "application", type: :stylesheet # => /assets/application.css
# asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
def asset_path(source, options = {})
- source = source.to_s
return "" unless source.present?
+ source = source.to_s
return source if source =~ URI_REGEXP
tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')
@@ -134,11 +137,11 @@ 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}"
@@ -147,7 +150,14 @@ module ActionView
# 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.
+ # will be the same. If :host options is set, it overwrites global
+ # +config.action_controller.asset_host+ setting.
+ #
+ # All other options provided are forwarded to +asset_path+ call.
+ #
+ # asset_url "application.js" # => http://example.com/assets/application.js
+ # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
+ #
def asset_url(source, options = {})
path_to_asset(source, options.merge(:protocol => :request))
end
@@ -191,8 +201,8 @@ module ActionView
# (proc or otherwise).
def compute_asset_host(source = "", options = {})
request = self.request if respond_to?(:request)
- host = config.asset_host if defined? config.asset_host
- host ||= request.base_url if request && options[:protocol] == :request
+ host = options[:host]
+ host ||= config.asset_host if defined? config.asset_host
if host.respond_to?(:call)
arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
@@ -203,6 +213,7 @@ module ActionView
host = host % (Zlib.crc32(source) % 4)
end
+ host ||= request.base_url if request && options[:protocol] == :request
return unless host
if host =~ URI_REGEXP
@@ -220,13 +231,13 @@ module ActionView
end
end
- # Computes the path to a javascript asset in the public javascripts directory.
+ # Computes the path to a JavaScript asset in the public javascripts directory.
# If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
# Full paths from the document root will be passed through.
- # Used internally by javascript_include_tag to build the script path.
+ # Used internally by +javascript_include_tag+ to build the script path.
#
- # javascript_path "xmlhr" # => /javascripts/xmlhr.js
- # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
+ # javascript_path "xmlhr" # => /assets/xmlhr.js
+ # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js
# javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
# javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr
# javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
@@ -235,7 +246,7 @@ module ActionView
end
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
- # Computes the full URL to a javascript asset in the public javascripts directory.
+ # Computes the full URL to a JavaScript asset in the public javascripts directory.
# This will use +javascript_path+ internally, so most of their behaviors will be the same.
def javascript_url(source, options = {})
url_to_asset(source, {type: :javascript}.merge!(options))
@@ -243,12 +254,12 @@ module ActionView
alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route
# Computes the path to a stylesheet asset in the public stylesheets directory.
- # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs).
+ # If the +source+ filename has no extension, .css will be appended (except for explicit URIs).
# Full paths from the document root will be passed through.
# Used internally by +stylesheet_link_tag+ to build the stylesheet path.
#
- # stylesheet_path "style" # => /stylesheets/style.css
- # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
+ # stylesheet_path "style" # => /assets/style.css
+ # stylesheet_path "dir/style.css" # => /assets/dir/style.css
# stylesheet_path "/dir/style.css" # => /dir/style.css
# stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style
# stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css
@@ -334,9 +345,9 @@ module ActionView
# Computes the path to a font asset.
# Full paths from the document root will be passed through.
#
- # font_path("font") # => /assets/font
- # font_path("font.ttf") # => /assets/font.ttf
- # font_path("dir/font.ttf") # => /assets/dir/font.ttf
+ # font_path("font") # => /fonts/font
+ # font_path("font.ttf") # => /fonts/font.ttf
+ # font_path("dir/font.ttf") # => /fonts/dir/font.ttf
# font_path("/dir/font.ttf") # => /dir/font.ttf
# font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf
def font_path(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 af70a4242a..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
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index b3af1d4da4..4db8930a26 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -11,7 +11,7 @@ module ActionView
# 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/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb
index 5afe435459..75d1634b2e 100644
--- a/actionview/lib/action_view/helpers/capture_helper.rb
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -202,15 +202,6 @@ module ActionView
ensure
self.output_buffer = old_buffer
end
-
- # Add the output buffer to the response body and start a new one.
- def flush_output_buffer #:nodoc:
- if output_buffer && !output_buffer.empty?
- response.stream.write output_buffer
- self.output_buffer = output_buffer.respond_to?(:clone_empty) ? output_buffer.clone_empty : output_buffer[0, 0]
- nil
- end
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 698f0ca31c..27c7a26098 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:
@@ -120,11 +124,11 @@ module ActionView
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)
@@ -149,8 +153,8 @@ module ActionView
#
# Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>.
#
- def time_ago_in_words(from_time, include_seconds_or_options = {})
- distance_of_time_in_words(from_time, Time.now, include_seconds_or_options)
+ def time_ago_in_words(from_time, options = {})
+ distance_of_time_in_words(from_time, Time.now, options)
end
alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
@@ -961,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]
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index c29c1b1eea..ba47eee9ba 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -11,24 +11,20 @@ module ActionView
# If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
# Useful for inspecting an object at the time of rendering.
#
- # @user = User.new({ username: 'testing', password: 'xyz', age: 42}) %>
+ # @user = User.new({ username: 'testing', password: 'xyz', age: 42})
# debug(@user)
# # =>
# <pre class='debug_dump'>--- !ruby/object:User
# attributes:
- # &nbsp; updated_at:
- # &nbsp; username: testing
- #
- # &nbsp; age: 42
- # &nbsp; password: xyz
- # &nbsp; created_at:
- # attributes_cache: {}
- #
- # new_record: true
+ # updated_at:
+ # username: testing
+ # age: 42
+ # password: xyz
+ # created_at:
# </pre>
def debug(object)
Marshal::dump(object)
- object = ERB::Util.html_escape(object.to_yaml).gsub(" ", "&nbsp; ").html_safe
+ object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, :class => "debug_dump")
rescue Exception # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 5235962f9f..c6bc0c9e38 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -434,7 +434,8 @@ module ActionView
output = capture(builder, &block)
html_options[:multipart] ||= builder.multipart?
- form_tag(options[:url] || {}, html_options) { output }
+ html_options = html_options_for_form(options[:url] || {}, html_options)
+ form_tag_with_body(html_options, output)
end
def apply_form_for_options!(record, object, options) #:nodoc:
@@ -449,7 +450,11 @@ module ActionView
method: method
)
- options[:url] ||= polymorphic_path(record, format: options.delete(:format))
+ options[:url] ||= if options.key?(:format)
+ polymorphic_path(record, format: options.delete(:format))
+ else
+ polymorphic_path(record, {})
+ end
end
private :apply_form_for_options!
@@ -457,7 +462,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
@@ -477,7 +482,7 @@ module ActionView
# Admin? : <%= permission_fields.check_box :admin %>
# <% end %>
#
- # <%= f.submit %>
+ # <%= person_form.submit %>
# <% end %>
#
# In this case, the checkbox field will be represented by an HTML +input+
@@ -746,6 +751,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
@@ -1007,6 +1013,18 @@ module ActionView
# date_field("user", "born_on", value: "1984-05-12")
# # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" />
#
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # date_field("user", "born_on", min: Date.today)
+ # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 date as the
+ # values for "min" and "max."
+ #
+ # date_field("user", "born_on", min: "2014-05-20")
+ # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
+ #
def date_field(object_name, method, options = {})
Tags::DateField.new(object_name, method, self, options).render
end
@@ -1024,6 +1042,18 @@ module ActionView
# time_field("task", "started_at")
# # => <input id="task_started_at" name="task[started_at]" type="time" />
#
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # time_field("task", "started_at", min: Time.now)
+ # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 time as the
+ # values for "min" and "max."
+ #
+ # time_field("task", "started_at", min: "01:00:00")
+ # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
+ #
def time_field(object_name, method, options = {})
Tags::TimeField.new(object_name, method, self, options).render
end
@@ -1041,6 +1071,18 @@ module ActionView
# datetime_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" />
#
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # datetime_field("user", "born_on", min: Date.today)
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 datetime
+ # with UTC offset as the values for "min" and "max."
+ #
+ # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
+ #
def datetime_field(object_name, method, options = {})
Tags::DatetimeField.new(object_name, method, self, options).render
end
@@ -1058,6 +1100,18 @@ module ActionView
# datetime_local_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" />
#
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # datetime_local_field("user", "born_on", min: Date.today)
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 datetime as
+ # the values for "min" and "max."
+ #
+ # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
+ #
def datetime_local_field(object_name, method, options = {})
Tags::DatetimeLocalField.new(object_name, method, self, options).render
end
@@ -1268,7 +1322,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 f625a9ff49..8ade7c6a74 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -14,81 +14,81 @@ module ActionView
#
# * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
#
- # select("post", "category", Post::CATEGORIES, {include_blank: true})
+ # select("post", "category", Post::CATEGORIES, {include_blank: true})
#
- # could become:
+ # could become:
#
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
- # </select>
+ # <select name="post[category]">
+ # <option></option>
+ # <option>joke</option>
+ # <option>poem</option>
+ # </select>
#
- # Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
+ # Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
#
- # Example with @post.person_id => 2:
+ # Example with <tt>@post.person_id => 2</tt>:
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'})
+ # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'})
#
- # could become:
+ # could become:
#
- # <select name="post[person_id]">
- # <option value="">None</option>
- # <option value="1">David</option>
- # <option value="2" selected="selected">Sam</option>
- # <option value="3">Tobias</option>
- # </select>
+ # <select name="post[person_id]">
+ # <option value="">None</option>
+ # <option value="1">David</option>
+ # <option value="2" selected="selected">Sam</option>
+ # <option value="3">Tobias</option>
+ # </select>
#
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
#
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'})
+ # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'})
#
- # could become:
+ # could become:
#
- # <select name="post[person_id]">
- # <option value="">Select Person</option>
- # <option value="1">David</option>
- # <option value="2">Sam</option>
- # <option value="3">Tobias</option>
- # </select>
+ # <select name="post[person_id]">
+ # <option value="">Select Person</option>
+ # <option value="1">David</option>
+ # <option value="2">Sam</option>
+ # <option value="3">Tobias</option>
+ # </select>
#
- # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
- # option to be in the +html_options+ parameter.
+ # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
+ # option to be in the +html_options+ parameter.
#
- # select("album[]", "genre", %w[rap rock country], {}, { index: nil })
+ # select("album[]", "genre", %w[rap rock country], {}, { index: nil })
#
- # becomes:
+ # becomes:
#
- # <select name="album[][genre]" id="album__genre">
- # <option value="rap">rap</option>
- # <option value="rock">rock</option>
- # <option value="country">country</option>
- # </select>
+ # <select name="album[][genre]" id="album__genre">
+ # <option value="rap">rap</option>
+ # <option value="rock">rock</option>
+ # <option value="country">country</option>
+ # </select>
#
# * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
#
- # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'})
+ # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'})
#
- # could become:
+ # could become:
#
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
- # <option disabled="disabled">restricted</option>
- # </select>
+ # <select name="post[category]">
+ # <option></option>
+ # <option>joke</option>
+ # <option>poem</option>
+ # <option disabled="disabled">restricted</option>
+ # </select>
#
- # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
+ # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
#
- # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }})
+ # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }})
#
- # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
- # <select name="post[category_id]">
- # <option value="1" disabled="disabled">2008 stuff</option>
- # <option value="2" disabled="disabled">Christmas</option>
- # <option value="3">Jokes</option>
- # <option value="4">Poems</option>
- # </select>
+ # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
+ # <select name="post[category_id]">
+ # <option value="1" disabled="disabled">2008 stuff</option>
+ # <option value="2" disabled="disabled">Christmas</option>
+ # <option value="3">Jokes</option>
+ # <option value="4">Poems</option>
+ # </select>
#
module FormOptionsHelper
# ERB::Util can mask some helpers like textilize. Make sure to include them.
@@ -152,11 +152,9 @@ module ActionView
# To prevent this the helper generates an auxiliary hidden field before
# every multiple select. The hidden field has the same name as multiple select and blank value.
#
- # This way, the client either sends only the hidden field (representing
- # the deselected multiple select box), or both fields. Since the HTML specification
- # says key/value pairs have to be sent in the same order they appear in the
- # form, and parameters extraction gets the last occurrence of any repeated
- # key in the query string, that works for ordinary forms.
+ # <b>Note:</b> The client either sends only the hidden field (representing
+ # the deselected multiple select box), or both fields. This means that the resulting array
+ # always contains a blank string.
#
# In case if you don't want the helper to generate this hidden field you can specify
# <tt>include_hidden: false</tt> option.
@@ -360,8 +358,8 @@ module ActionView
html_attributes = option_html_attributes(element)
text, value = option_text_and_value(element).map { |item| item.to_s }
- html_attributes[:selected] = option_value_selected?(value, selected)
- html_attributes[:disabled] = 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)
@@ -463,21 +461,7 @@ module ActionView
end
# Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
- # wraps them with <tt><optgroup></tt> tags.
- #
- # Parameters:
- # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
- # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
- # nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
- # Ex. ["North America",[["United States","US"],["Canada","CA"]]]
- # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
- # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
- # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
- #
- # Options:
- # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this
- # prepends an option with a generic prompt - "Please select" - or the given prompt string.
- # * <tt>:divider</tt> - the divider for the options groups.
+ # wraps them with <tt><optgroup></tt> tags:
#
# grouped_options = [
# ['North America',
@@ -504,22 +488,36 @@ module ActionView
# <option value="France">France</option>
# </optgroup>
#
- # grouped_options = [
- # [['United States','US'], 'Canada'],
- # ['Denmark','Germany','France']
- # ]
- # grouped_options_for_select(grouped_options, nil, divider: '---------')
+ # Parameters:
+ # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
+ # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
+ # nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
+ # Ex. ["North America",[["United States","US"],["Canada","CA"]]]
+ # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
+ # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
+ # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
#
- # Possible output:
- # <optgroup label="---------">
- # <option value="US">United States</option>
- # <option value="Canada">Canada</option>
- # </optgroup>
- # <optgroup label="---------">
- # <option value="Denmark">Denmark</option>
- # <option value="Germany">Germany</option>
- # <option value="France">France</option>
- # </optgroup>
+ # Options:
+ # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this
+ # prepends an option with a generic prompt - "Please select" - or the given prompt string.
+ # * <tt>:divider</tt> - the divider for the options groups.
+ #
+ # grouped_options = [
+ # [['United States','US'], 'Canada'],
+ # ['Denmark','Germany','France']
+ # ]
+ # grouped_options_for_select(grouped_options, nil, divider: '---------')
+ #
+ # Possible output:
+ # <optgroup label="---------">
+ # <option value="US">United States</option>
+ # <option value="Canada">Canada</option>
+ # </optgroup>
+ # <optgroup label="---------">
+ # <option value="Denmark">Denmark</option>
+ # <option value="Germany">Germany</option>
+ # <option value="France">France</option>
+ # </optgroup>
#
# <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
# wrap the output in an appropriate <tt><select></tt> tag.
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 80f066b3be..b18f578183 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -67,7 +67,7 @@ module ActionView
def form_tag(url_for_options = {}, options = {}, &block)
html_options = html_options_for_form(url_for_options, options)
if block_given?
- form_tag_in_block(html_options, &block)
+ form_tag_with_body(html_options, capture(&block))
else
form_tag_html(html_options)
end
@@ -82,14 +82,18 @@ module ActionView
# ==== Options
# * <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>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty.
+ # * <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>
#
@@ -105,13 +109,16 @@ module ActionView
# # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option>
# # <option>Out</option></select>
#
- # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input'
- # # => <select class="form_input" id="access" multiple="multiple" name="access[]"><option>Read</option>
+ # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, multiple: true, class: 'form_input', id: 'unique_id'
+ # # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option>
# # <option>Write</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
# # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select>
#
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
+ # # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
+ #
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something"
# # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
#
@@ -465,17 +472,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.
@@ -495,19 +508,19 @@ module ActionView
#
# ==== Examples
# image_submit_tag("login.png")
- # # => <input alt="Login" src="/images/login.png" type="image" />
+ # # => <input alt="Login" src="/assets/login.png" type="image" />
#
# image_submit_tag("purchase.png", disabled: true)
- # # => <input alt="Purchase" disabled="disabled" src="/images/purchase.png" type="image" />
+ # # => <input alt="Purchase" disabled="disabled" src="/assets/purchase.png" type="image" />
#
# image_submit_tag("search.png", class: 'search_button', alt: 'Find')
- # # => <input alt="Find" class="search_button" src="/images/search.png" type="image" />
+ # # => <input alt="Find" class="search_button" src="/assets/search.png" type="image" />
#
# image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button")
- # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" />
+ # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
#
# image_submit_tag("save.png", data: { confirm: "Are you sure?" })
- # # => <input alt="Save" src="/images/save.png" data-confirm="Are you sure?" type="image" />
+ # # => <input alt="Save" src="/assets/save.png" data-confirm="Are you sure?" type="image" />
def image_submit_tag(source, options = {})
options = options.stringify_keys
tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options)
@@ -544,6 +557,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
@@ -552,6 +578,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
@@ -560,6 +599,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
@@ -569,6 +621,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
@@ -632,6 +697,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
@@ -640,6 +718,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
@@ -651,12 +742,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"
@@ -677,7 +796,10 @@ module ActionView
# Creates the hidden UTF8 enforcer tag. Override this method in a helper
# to customize the tag.
def utf8_enforcer_tag
- tag(:input, :type => "hidden", :name => "utf8", :value => "&#x2713;".html_safe)
+ # Use raw HTML to ensure the value is written as an HTML entity; it
+ # needs to be the right character regardless of which encoding the
+ # browser infers.
+ '<input name="utf8" type="hidden" value="&#x2713;" />'.html_safe
end
private
@@ -720,9 +842,11 @@ module ActionView
method_tag(method) + token_tag(authenticity_token)
end
- enforce_utf8 = html_options.delete("enforce_utf8") { true }
- tags = (enforce_utf8 ? utf8_enforcer_tag : ''.html_safe) << method_tag
- content_tag(:div, tags, :style => 'display:none')
+ if html_options.delete("enforce_utf8") { true }
+ utf8_enforcer_tag + method_tag
+ else
+ method_tag
+ end
end
def form_tag_html(html_options)
@@ -730,8 +854,7 @@ module ActionView
tag(:form, html_options, true) + extra_tags
end
- def form_tag_in_block(html_options, &block)
- content = capture(&block)
+ def form_tag_with_body(html_options, content)
output = form_tag_html(html_options)
output << content
output.safe_concat("</form>")
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index e475d5b018..629c447f3f 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -47,7 +47,13 @@ module ActionView
# tag.
#
# javascript_tag "alert('All is good')", defer: 'defer'
- # # => <script defer="defer">alert('All is good')</script>
+ #
+ # Returns:
+ # <script defer="defer">
+ # //<![CDATA[
+ # alert('All is good')
+ # //]]>
+ # </script>
#
# Instead of passing the content as an argument, you can also use a block
# in which case, you pass your +html_options+ as the first parameter.
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index ad825cd1f1..7220bded3c 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -266,14 +266,8 @@ module ActionView
# number_to_human_size(1234567, precision: 2) # => 1.2 MB
# number_to_human_size(483989, precision: 2) # => 470 KB
# number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
- #
- # Non-significant zeros after the fractional separator are
- # stripped out by default (set
- # <tt>:strip_insignificant_zeros</tt> to +false+ to change
- # that):
- #
- # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
- # number_to_human_size(524288000, precision: 5) # => "500 MB"
+ # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
+ # number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
delegate_number_helper_method(:number_to_human_size, number, options)
end
@@ -343,11 +337,15 @@ module ActionView
# separator: ',',
# significant: false) # => "1,2 Million"
#
+ # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12345012345, significant: false) # => "12.345 Billion"
+ #
# Non-significant zeros after the decimal separator are stripped
# out by default (set <tt>:strip_insignificant_zeros</tt> to
# +false+ to change that):
- # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion"
- # number_to_human(500000000, precision: 5) # => "500 Million"
+ #
+ # number_to_human(12.00001) # => "12"
+ # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
#
# ==== Custom Unit Quantifiers
#
@@ -384,20 +382,29 @@ module ActionView
def delegate_number_helper_method(method, number, options)
return unless number
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+ options = escape_unsafe_options(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
ActiveSupport::NumberHelper.public_send(method, number, options)
}
end
- 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?
- options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].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/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb
index 60a4478c26..f03362d0f5 100644
--- a/actionview/lib/action_view/helpers/output_safety_helper.rb
+++ b/actionview/lib/action_view/helpers/output_safety_helper.rb
@@ -17,10 +17,10 @@ module ActionView #:nodoc:
stringish.to_s.html_safe
end
- # This method returns a html safe string similar to what <tt>Array#join</tt>
- # would return. All items in the array, including the supplied separator, are
- # html escaped unless they are html safe, and the returned string is marked
- # as html safe.
+ # This method returns an html safe string similar to what <tt>Array#join</tt>
+ # would return. The array is flattened, and all items, including
+ # the supplied separator, are html escaped unless they are html
+ # safe, and the returned string is marked as html safe.
#
# safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
# # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
@@ -29,9 +29,9 @@ module ActionView #:nodoc:
# # => "<p>foo</p><br /><p>bar</p>"
#
def safe_join(array, sep=$,)
- sep = ERB::Util.html_escape(sep)
+ sep = ERB::Util.unwrapped_html_escape(sep)
- array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe
+ array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
end
end
end
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 458086de96..6cd6e858dd 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/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index e5cb843670..049af275b6 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -48,7 +48,7 @@ module ActionView
# Change allowed default attributes
#
# class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
+ # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style']
# end
#
# Please note that sanitizing user-provided text does not guarantee that the
@@ -204,7 +204,7 @@ module ActionView
# Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
#
# class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc'
+ # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
# end
#
def sanitized_allowed_attributes=(attributes)
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index 3528381781..268558669e 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -9,6 +9,7 @@ module ActionView
module TagHelper
extend ActiveSupport::Concern
include CaptureHelper
+ include OutputSafetyHelper
BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
autoplay controls loop selected hidden scoped async
@@ -42,7 +43,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 +58,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 +80,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 +89,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>
#
@@ -133,7 +140,7 @@ module ActionView
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
- content = ERB::Util.h(content) if escape
+ content = ERB::Util.unwrapped_html_escape(content) if escape
"<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
end
@@ -167,8 +174,11 @@ module ActionView
end
def tag_option(key, value, escape)
- value = value.join(" ") if value.is_a?(Array)
- value = ERB::Util.h(value) if escape
+ if value.is_a?(Array)
+ value = escape ? safe_join(value, " ") : value.join(" ")
+ else
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value
+ end
%(#{key}="#{value}")
end
end
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 9b77ebeb1b..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,10 +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_name = @html_options[:name] || "#{tag_name}[]"
- hidden = @template_object.hidden_field_tag(hidden_name, "", :id => nil)
-
- rendered_collection + hidden
+ if @options.fetch(:include_hidden, true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
end
private
@@ -38,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 991f32cea2..8050638363 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -44,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/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb
index 25e7e05ec6..b2cee9d198 100644
--- a/actionview/lib/action_view/helpers/tags/datetime_field.rb
+++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb
@@ -5,8 +5,8 @@ module ActionView
def render
options = @options.stringify_keys
options["value"] ||= format_date(value(object))
- options["min"] = format_date(options["min"])
- options["max"] = format_date(options["max"])
+ options["min"] = format_date(datetime_value(options["min"]))
+ options["max"] = format_date(datetime_value(options["max"]))
@options = options
super
end
@@ -16,6 +16,14 @@ module ActionView
def format_date(value)
value.try(:strftime, "%Y-%m-%dT%T.%L%z")
end
+
+ def datetime_value(value)
+ if value.is_a? String
+ DateTime.parse(value) rescue nil
+ else
+ value
+ end
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
index 35d3ba8434..a5bcaf8153 100644
--- a/actionview/lib/action_view/helpers/tags/label.rb
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -35,9 +35,9 @@ module ActionView
if block_given?
content = @template_object.capture(&block)
else
+ method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
content = if @content.blank?
- @object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1')
- method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
+ @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
if object.respond_to?(:to_model)
key = object.class.model_name.i18n_key
@@ -51,7 +51,7 @@ module ActionView
end
content ||= if object && object.class.respond_to?(:human_attribute_name)
- object.class.human_attribute_name(@method_name)
+ object.class.human_attribute_name(method_and_value)
end
content ||= @method_name.humanize
diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb
index 00881d9978..180900cc8d 100644
--- a/actionview/lib/action_view/helpers/tags/select.rb
+++ b/actionview/lib/action_view/helpers/tags/select.rb
@@ -3,7 +3,7 @@ module ActionView
module Tags # :nodoc:
class Select < Base # :nodoc:
def initialize(object_name, method_name, template_object, choices, options, html_options)
- @choices = block_given? ? template_object.capture { yield } : choices
+ @choices = block_given? ? template_object.capture { yield || "" } : choices
@choices = @choices.to_a if @choices.is_a?(Range)
@html_options = html_options
diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
index e910879ebf..e0b80d81c2 100644
--- a/actionview/lib/action_view/helpers/tags/text_field.rb
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -7,7 +7,6 @@ module ActionView
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["value"] &&= ERB::Util.html_escape(options["value"])
add_default_name_and_id(options)
tag("input", options)
end
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 7cfbca5b6f..b859653bc9 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -103,11 +103,14 @@ module ActionView
# Highlights one or more +phrases+ everywhere in +text+ by inserting it into
# a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
# as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
- # '<mark>\1</mark>')
+ # '<mark>\1</mark>') or passing a block that receives each matched term.
#
# highlight('You searched for: rails', 'rails')
# # => You searched for: <mark>rails</mark>
#
+ # highlight('You searched for: rails', /for|rails/)
+ # # => You searched <mark>for</mark>: <mark>rails</mark>
+ #
# highlight('You searched for: ruby, rails, dhh', 'actionpack')
# # => You searched for: ruby, rails, dhh
#
@@ -116,15 +119,25 @@ module ActionView
#
# highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
# # => You searched for: <a href="search?q=rails">rails</a>
+ #
+ # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
+ # # => You searched for: <a href="search?q=rails">rails</a>
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
if text.blank? || phrases.blank?
text
else
- highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
- match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
- text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
+ match = Array(phrases).map do |p|
+ Regexp === p ? p.to_s : Regexp.escape(p)
+ end.join('|')
+
+ if block_given?
+ text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
+ else
+ highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
+ text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
+ end
end.html_safe
end
@@ -155,9 +168,13 @@ module ActionView
def excerpt(text, phrase, options = {})
return unless text && phrase
- separator = options[:separator] || ''
- phrase = Regexp.escape(phrase)
- regex = /#{phrase}/i
+ separator = options.fetch(:separator, nil) || ""
+ case phrase
+ when Regexp
+ regex = phrase
+ else
+ regex = /#{Regexp.escape(phrase)}/i
+ end
return unless matches = text.match(regex)
phrase = matches[0]
@@ -171,7 +188,7 @@ module ActionView
end
end
- first_part, second_part = text.split(regex, 2)
+ first_part, second_part = text.split(phrase, 2)
prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 0bc40874d9..1d50ea2ff5 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,4 +1,5 @@
require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/string/access'
require 'i18n/exceptions'
module ActionView
@@ -7,7 +8,7 @@ module ActionView
module TranslationHelper
# Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
#
- # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
+ # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
# into inline spans that:
#
# * have a "translation-missing" class set,
@@ -34,6 +35,7 @@ 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 = options.dup
options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
# If the user has specified rescue_format then pass it all through, otherwise use
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 3ccace1274..c3be47133c 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -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
@@ -323,7 +323,7 @@ module ActionView
inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
end
end
- content_tag('form', content_tag('div', inner_tags), form_options)
+ content_tag('form', inner_tags, form_options)
end
# Creates a link tag of the given +name+ using a URL created by the set of
@@ -389,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
@@ -421,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
@@ -462,8 +462,6 @@ module ActionView
# <strong>Email me:</strong> <span>me@domain.com</span>
# </a>
def mail_to(email_address, name = nil, html_options = {}, &block)
- email_address = ERB::Util.html_escape(email_address)
-
html_options, name = name, nil if block_given?
html_options = (html_options || {}).stringify_keys
@@ -471,11 +469,11 @@ module ActionView
option = html_options.delete(item) || next
"#{item}=#{Rack::Utils.escape_path(option)}"
}.compact
- extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&'))
+ extras = extras.empty? ? '' : '?' + extras.join('&')
- html_options["href"] = "mailto:#{email_address}#{extras}".html_safe
+ html_options["href"] = "mailto:#{email_address}#{extras}"
- content_tag(:a, name || email_address.html_safe, html_options, &block)
+ content_tag(:a, name || email_address, html_options, &block)
end
# True if the current request URI was generated by the given +options+.
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index ffa67649da..9ee05bd816 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -420,7 +420,7 @@ module ActionView
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 6c8d9cb5bf..9047dbdd85 100644
--- a/actionview/lib/action_view/log_subscriber.rb
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -13,11 +13,11 @@ module ActionView
end
def render_template(event)
- return unless logger.info?
- message = " Rendered #{from_rails_root(event.payload[:identifier])}"
- message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
- message << " (#{event.duration.round(1)}ms)"
- info(message)
+ info do
+ message = " Rendered #{from_rails_root(event.payload[:identifier])}"
+ message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
+ message << " (#{event.duration.round(1)}ms)"
+ end
end
alias :render_partial :render_template
alias :render_collection :render_template
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 76c9890776..ea687d9cca 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -66,10 +66,7 @@ module ActionView
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
- }
+ details[:formats] &= Mime::SET.symbols
end
@details_keys[details] ||= new
end
@@ -114,7 +111,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))
@@ -134,7 +131,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|
@@ -159,7 +157,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
@@ -220,7 +225,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
@@ -231,7 +236,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/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index 73c19a0ae2..1f122f9bc6 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -29,8 +29,9 @@ module ActionView
def extract_details(options)
@lookup_context.registered_details.each_with_object({}) do |key, details|
- next unless value = options[key]
- details[key] = Array(value)
+ value = options[key]
+
+ details[key] = Array(value) if value
end
end
@@ -41,6 +42,7 @@ module ActionView
def prepend_formats(formats)
formats = Array(formats)
return if formats.empty? || @lookup_context.html_fallback_for_js
+
@lookup_context.formats = formats | @lookup_context.formats
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 36f17f01fd..0407632435 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,6 +1,33 @@
require 'thread_safe'
module ActionView
+ class PartialIteration
+ # The number of iterations that will be done by the partial.
+ attr_reader :size
+
+ # The current iteration of the partial.
+ attr_reader :index
+
+ def initialize(size)
+ @size = size
+ @index = 0
+ end
+
+ # Check if this is the first iteration of the partial.
+ def first?
+ index == 0
+ end
+
+ # Check if this is the last iteration of the partial.
+ def last?
+ index == size - 1
+ end
+
+ def iterate! # :nodoc:
+ @index += 1
+ end
+ end
+
# = Action View Partials
#
# There's also a convenience method for rendering sub templates within the current controller that depends on a
@@ -56,8 +83,12 @@ module ActionView
# <%= render partial: "ad", collection: @advertisements %>
#
# This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An
- # iteration counter will automatically be made available to the template with a name of the form
- # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
+ # iteration object will automatically be made available to the template with a name of the form
+ # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
+ # the collection and the total size of the collection. The iteration object also has two convenience methods,
+ # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
+ # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
+ # +index+ method.
#
# The <tt>:as</tt> option may be used when rendering partials.
#
@@ -281,6 +312,8 @@ module ActionView
end
end
+ private
+
def render_collection
return nil if @collection.blank?
@@ -322,25 +355,27 @@ module ActionView
# respond to +to_partial_path+ in order to setup the path.
def setup(context, options, block)
@view = context
- partial = options[:partial]
-
@options = options
- @locals = options[:locals] || {}
@block = block
+
+ @locals = options[:locals] || {}
@details = extract_details(options)
prepend_formats(options[:formats])
+ partial = options[:partial]
+
if String === partial
@object = options[:object]
+ @collection = collection_from_options
@path = partial
- @collection = collection
else
@object = partial
+ @collection = collection_from_object || collection_from_options
- if @collection = collection_from_object || collection
+ if @collection
paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.size == 1 ? paths.first : nil
+ @path = paths.uniq.one? ? paths.first : nil
else
@path = partial_path
end
@@ -352,7 +387,7 @@ module ActionView
end
if @path
- @variable, @variable_counter = retrieve_variable(@path, as)
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
@template_keys = retrieve_template_keys
else
paths.map! { |path| retrieve_variable(path, as).unshift(path) }
@@ -361,7 +396,7 @@ module ActionView
self
end
- def collection
+ def collection_from_options
if @options.key?(:collection)
collection = @options[:collection]
collection.respond_to?(:to_ary) ? collection.to_ary : []
@@ -373,9 +408,7 @@ module ActionView
end
def find_partial
- if path = @path
- find_template(path, @template_keys)
- end
+ find_template(@path, @template_keys) if @path
end
def find_template(path, locals)
@@ -385,19 +418,22 @@ module ActionView
def collection_with_template
view, locals, template = @view, @locals, @template
- as, counter = @variable, @variable_counter
+ as, counter, iteration = @variable, @variable_counter, @variable_iteration
if layout = @options[:layout]
layout = find_template(layout, @template_keys)
end
- index = -1
+ partial_iteration = PartialIteration.new(@collection.size)
+ locals[iteration] = partial_iteration
+
@collection.map do |object|
- locals[as] = object
- locals[counter] = (index += 1)
+ locals[as] = object
+ locals[counter] = partial_iteration.index
content = template.render(view, locals)
content = layout.render(view, locals) { content } if layout
+ partial_iteration.iterate!
content
end
end
@@ -407,16 +443,20 @@ module ActionView
cache = {}
keys = @locals.keys
- index = -1
+ partial_iteration = PartialIteration.new(@collection.size)
+
@collection.map do |object|
- index += 1
- path, as, counter = collection_data[index]
+ index = partial_iteration.index
+ path, as, counter, iteration = collection_data[index]
- locals[as] = object
- locals[counter] = index
+ locals[as] = object
+ locals[counter] = index
+ locals[iteration] = partial_iteration
template = (cache[path] ||= find_template(path, keys + [as, counter]))
- template.render(view, locals)
+ content = template.render(view, locals)
+ partial_iteration.iterate!
+ content
end
end
@@ -466,8 +506,11 @@ module ActionView
def retrieve_template_keys
keys = @locals.keys
- keys << @variable if @object || @collection
- keys << @variable_counter if @collection
+ keys << @variable if @object || @collection
+ if @collection
+ keys << @variable_counter
+ keys << @variable_iteration
+ end
keys
end
@@ -477,8 +520,11 @@ module ActionView
raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
$1.to_sym
end
- variable_counter = :"#{variable}_counter" if @collection
- [variable, variable_counter]
+ if @collection
+ variable_counter = :"#{variable}_counter"
+ variable_iteration = :"#{variable}_iteration"
+ end
+ [variable, variable_counter, variable_iteration]
end
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index 668831dff3..f3a48ecfa0 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -6,23 +6,28 @@ module ActionView
@view = context
@details = extract_details(options)
template = determine_template(options)
- context = @lookup_context
prepend_formats(template.formats)
- unless context.rendered_format
- context.rendered_format = template.formats.first || formats.first
- end
+ @lookup_context.rendered_format ||= (template.formats.first || formats.first)
render_template(template, options[:layout], options[:locals])
end
+ private
+
# Determine the template to be rendered using the given options.
- def determine_template(options) #:nodoc:
+ def determine_template(options)
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 +40,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
index 7c17220d14..81d5836a8c 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -35,12 +35,13 @@ module ActionView
module ClassMethods
def view_context_class
@view_context_class ||= begin
- routes = respond_to?(:_routes) && _routes
+ include_path_helpers = supports_path?
+ routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
- include routes.url_helpers
+ include routes.url_helpers(include_path_helpers)
include routes.mounted_helpers
end
@@ -62,8 +63,8 @@ module ActionView
#
# 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]
+ # Create a new ActionView instance for a controller and we can also pass the arguments.
+ # View#render(option)
# Returns String with the rendered template
#
# Override this method in a module to change the default behavior.
@@ -100,7 +101,7 @@ module ActionView
end
# Assign the rendered format to lookup context.
- def _process_format(format) #:nodoc:
+ def _process_format(format, options = {}) #:nodoc:
super
lookup_context.formats = [format.to_sym]
lookup_context.rendered_format = lookup_context.formats.first
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
index 33be06cbf7..881a123572 100644
--- a/actionview/lib/action_view/routing_url_for.rb
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -1,3 +1,5 @@
+require 'action_dispatch/routing/polymorphic_routes'
+
module ActionView
module RoutingUrlFor
@@ -77,16 +79,20 @@ 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 Symbol
+ ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_string_call self, options
when Array
polymorphic_path(options, options.extract_options!)
+ when Class
+ ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_class_call self, options
else
- polymorphic_path(options)
+ ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call self, options
end
end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
index 1b9426c0e5..b39f7d583b 100644
--- a/actionview/lib/action_view/tasks/dependencies.rake
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -2,16 +2,20 @@ 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
+ puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).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
+ puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).dependencies
+ end
+
+ def template_name
+ ENV['TEMPLATE'].split('.', 2).first
+ end
+
+ def finder
+ ApplicationController.new.lookup_context
end
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 9b0619f1aa..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
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index d9cddc0040..33bfcb458c 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -32,6 +32,15 @@ module ActionView #:nodoc:
@@template_extensions = nil
end
+ # Opposite to register_template_handler.
+ def unregister_template_handler(*extensions)
+ extensions.each do |extension|
+ handler = @@template_handlers.delete extension.to_sym
+ @@default_template_handlers = nil if @@default_template_handlers == handler
+ end
+ @@template_extensions = nil
+ end
+
def template_handler_extensions
@@template_handlers.keys.map {|key| key.to_s }.sort
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 3a3b74cdd5..d29d020c17 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -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
@@ -180,25 +181,41 @@ module ActionView
def query(path, details, formats)
query = build_query(path, details)
- # deals with case-insensitive file systems.
- sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
-
- template_paths = Dir[query].reject { |filename|
- File.directory?(filename) ||
- !sanitizer[File.dirname(filename)].include?(filename)
- }
+ template_paths = find_template_paths query
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
+ if RUBY_VERSION >= '2.2.0'
+ def find_template_paths(query)
+ Dir[query].reject { |filename|
+ File.directory?(filename) ||
+ # deals with case-insensitive file systems.
+ !File.fnmatch(query, filename, File::FNM_EXTGLOB)
+ }
+ end
+ else
+ def find_template_paths(query)
+ # deals with case-insensitive file systems.
+ sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
+
+ Dir[query].reject { |filename|
+ File.directory?(filename) ||
+ !sanitizer[File.dirname(filename)].include?(filename)
+ }
+ end
+ end
+
# Helper for building query glob string based on resolver's pattern.
def build_query(path, details)
query = @pattern.dup
@@ -225,10 +242,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,10 +257,10 @@ module ActionView
end
handler = Template.handler_for_extension(extension)
- format = pieces.last && pieces.last.split(EXTENSIONS[:variants], 2).first # remove variant from format
+ format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
format &&= Template::Types[format]
- [handler, format]
+ [handler, format, variant]
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/test_case.rb b/actionview/lib/action_view/test_case.rb
index 3145446114..d0da415c5d 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
@@ -258,7 +259,7 @@ module ActionView
def method_missing(selector, *args)
if @controller.respond_to?(:_routes) &&
- ( @controller._routes.named_routes.helpers.include?(selector) ||
+ ( @controller._routes.named_routes.route_defined?(selector) ||
@controller._routes.mounted_helpers.method_defined?(selector) )
@controller.__send__(selector, *args)
else
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index af53ad3b25..dfb7d463b4 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -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/version.rb b/actionview/lib/action_view/version.rb
index edb6d8f116..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.beta1"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActionView.version.segments
- STRING = ActionView.version.to_s
+ gem_version
end
end
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index 6c349feb1d..80a41f2418 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -14,27 +14,38 @@ module ActionView
:locale, :locale=, :to => :lookup_context
module ClassMethods
- def parent_prefixes
- @parent_prefixes ||= begin
- parent_controller = superclass
- prefixes = []
-
- until parent_controller.abstract?
- prefixes << parent_controller.controller_path
- parent_controller = parent_controller.superclass
- end
+ def _prefixes # :nodoc:
+ @_prefixes ||= begin
+ deprecated_prefixes = handle_deprecated_parent_prefixes
+ if deprecated_prefixes
+ deprecated_prefixes
+ else
+ return local_prefixes if superclass.abstract?
- prefixes
+ local_prefixes + superclass._prefixes
+ end
end
end
+
+ private
+
+ # Override this method in your controller if you want to change paths prefixes for finding views.
+ # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
+ def local_prefixes
+ [controller_path]
+ end
+
+ def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0.
+ return unless respond_to?(:parent_prefixes)
+
+ ActiveSupport::Deprecation.warn "Overriding ActionController::Base::parent_prefixes is deprecated, override .local_prefixes instead."
+ local_prefixes + parent_prefixes
+ end
end
# The prefixes used in render "foo" shortcuts.
- def _prefixes
- @_prefixes ||= begin
- parent_prefixes = self.class.parent_prefixes
- parent_prefixes.dup.unshift(controller_path)
- end
+ def _prefixes # :nodoc:
+ self.class._prefixes
end
# LookupContext is the object responsible to hold all information required to lookup
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 9eae3a4fbd..d60712255b 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -274,7 +274,6 @@ ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
module ActionController
class Base
- include ActionController::Testing
# This stub emulates the Railtie including the URL helpers from a Rails application
include SharedTestRoutes.url_helpers
include SharedTestRoutes.mounted_helpers
@@ -339,3 +338,5 @@ end
def jruby_skip(message = '')
skip message if defined?(JRUBY_VERSION)
end
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index 40d3b17131..e653b12d32 100644
--- a/actionview/test/actionpack/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -150,6 +150,54 @@ module AbstractController
end
end
+ class OverridingLocalPrefixes < AbstractController::Base
+ include AbstractController::Rendering
+ include ActionView::Rendering
+ append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
+
+ def index
+ render
+ end
+
+ def self.local_prefixes
+ # this would usually return "abstract_controller/testing/overriding_local_prefixes"
+ super + ["abstract_controller/testing/me3"]
+ end
+
+ class Inheriting < self
+ end
+ end
+
+ class OverridingLocalPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3.
+ test "overriding .local_prefixes adds prefix" do
+ @controller = OverridingLocalPrefixes.new
+ @controller.process(:index)
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+
+ test ".local_prefixes is inherited" do
+ @controller = OverridingLocalPrefixes::Inheriting.new
+ @controller.process(:index)
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+ end
+
+ class DeprecatedParentPrefixes < OverridingLocalPrefixes
+ def self.parent_prefixes
+ ["abstract_controller/testing/me3"]
+ end
+ end
+
+ class DeprecatedParentPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3.
+ test "overriding .parent_prefixes is deprecated" do
+ @controller = DeprecatedParentPrefixes.new
+ assert_deprecated do
+ @controller.process(:index)
+ end
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+ end
+
# Test rendering with layouts
# ====
# self._layout is used when defined
diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb
index f9d8c916d9..d09f91c1e2 100644
--- a/actionview/test/actionpack/abstract/render_test.rb
+++ b/actionview/test/actionpack/abstract/render_test.rb
@@ -60,42 +60,42 @@ module AbstractController
end
def test_render_template
- @controller.process(:template)
+ assert_equal "With Template", @controller.process(:template)
assert_equal "With Template", @controller.response_body
end
def test_render_file
- @controller.process(:file)
+ assert_equal "With File", @controller.process(:file)
assert_equal "With File", @controller.response_body
end
def test_render_inline
- @controller.process(:inline)
+ assert_equal "With Inline", @controller.process(:inline)
assert_equal "With Inline", @controller.response_body
end
def test_render_text
- @controller.process(:text)
+ assert_equal "With Text", @controller.process(:text)
assert_equal "With Text", @controller.response_body
end
def test_render_default
- @controller.process(:default)
+ assert_equal "With Default", @controller.process(:default)
assert_equal "With Default", @controller.response_body
end
def test_render_string
- @controller.process(:string)
+ assert_equal "With String", @controller.process(:string)
assert_equal "With String", @controller.response_body
end
def test_render_symbol
- @controller.process(:symbol)
+ assert_equal "With Symbol", @controller.process(:symbol)
assert_equal "With Symbol", @controller.response_body
end
def test_render_string_with_path
- @controller.process(:string_with_path)
+ assert_equal "With String With Path", @controller.process(:string_with_path)
assert_equal "With String With Path", @controller.response_body
end
end
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index b44f57a23e..bd345fe873 100644
--- a/actionview/test/actionpack/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -6,9 +6,6 @@ require 'active_support/core_ext/array/extract_options'
# method has access to the view_paths array when looking for a layout to automatically assign.
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/actionpack/layout_tests/' ]
class LayoutTest < ActionController::Base
@@ -17,6 +14,15 @@ class LayoutTest < ActionController::Base
self.view_paths = ActionController::Base.view_paths.dup
end
+module TemplateHandlerHelper
+ def with_template_handler(*extensions, handler)
+ ActionView::Template.register_template_handler(*extensions, handler)
+ yield
+ ensure
+ ActionView::Template.unregister_template_handler(*extensions)
+ end
+end
+
# Restore view_paths to previous value
ActionController::Base.view_paths = old_load_paths
@@ -39,6 +45,8 @@ class MultipleExtensions < LayoutTest
end
class LayoutAutoDiscoveryTest < ActionController::TestCase
+ include TemplateHandlerHelper
+
def setup
super
@request.host = "www.nextangle.com"
@@ -57,10 +65,12 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase
end
def test_third_party_template_library_auto_discovers_layout
- @controller = ThirdPartyTemplateLibraryController.new
- get :hello
- assert_response :success
- assert_equal 'layouts/third_party_template_library.mab', @response.body
+ with_template_handler :mab, lambda { |template| template.source.inspect } do
+ @controller = ThirdPartyTemplateLibraryController.new
+ get :hello
+ assert_response :success
+ assert_equal 'layouts/third_party_template_library.mab', @response.body
+ end
end
def test_namespaced_controllers_auto_detect_layouts1
@@ -135,6 +145,7 @@ end
class LayoutSetInResponseTest < ActionController::TestCase
include ActionView::Template::Handlers
+ include TemplateHandlerHelper
def test_layout_set_when_using_default_layout
@controller = DefaultLayoutController.new
@@ -191,9 +202,11 @@ class LayoutSetInResponseTest < ActionController::TestCase
end
def test_layout_set_when_using_render
- @controller = SetsLayoutInRenderController.new
- get :hello
- assert_template :layout => "layouts/third_party_template_library"
+ with_template_handler :mab, lambda { |template| template.source.inspect } do
+ @controller = SetsLayoutInRenderController.new
+ get :hello
+ assert_template :layout => "layouts/third_party_template_library"
+ end
end
def test_layout_is_not_set_when_none_rendered
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index 45b8049b83..b3b51ae583 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -536,6 +536,14 @@ class TestController < ApplicationController
render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
end
+ def partial_collection_with_iteration
+ render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ]
+ end
+
+ def partial_collection_with_as_and_iteration
+ render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client
+ end
+
def partial_collection_with_counter
render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
end
@@ -720,6 +728,11 @@ class RenderTest < ActionController::TestCase
assert_equal "Elastica", @response.body
end
+ def test_render_process
+ get :render_action_hello_world_as_string
+ assert_equal ["Hello world!"], @controller.process(:render_action_hello_world_as_string)
+ end
+
# :ported:
def test_render_from_variable
get :render_hello_world_from_variable
@@ -834,7 +847,7 @@ class RenderTest < ActionController::TestCase
def test_render_text_with_nil
get :render_text_with_nil
assert_response 200
- assert_equal ' ', @response.body
+ assert_equal '', @response.body
end
# :ported:
@@ -1022,7 +1035,7 @@ class RenderTest < ActionController::TestCase
def test_rendering_nothing_on_layout
get :rendering_nothing_on_layout
- assert_equal " ", @response.body
+ assert_equal '', @response.body
end
def test_render_to_string_doesnt_break_assigns
@@ -1232,6 +1245,16 @@ class RenderTest < ActionController::TestCase
assert_equal "david david davidmary mary mary", @response.body
end
+ def test_partial_collection_with_iteration
+ get :partial_collection_with_iteration
+ assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body
+ end
+
+ def test_partial_collection_with_as_and_iteration
+ get :partial_collection_with_as_and_iteration
+ assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body
+ end
+
def test_partial_collection_with_counter
get :partial_collection_with_counter
assert_equal "david0mary1", @response.body
@@ -1332,4 +1355,3 @@ class RenderTest < ActionController::TestCase
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/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb
index 95fbb112c0..cca55c9af4 100644
--- a/actionview/test/active_record_unit.rb
+++ b/actionview/test/active_record_unit.rb
@@ -57,7 +57,7 @@ class ActiveRecordTestConnector
end
end
- # Load actionpack sqlite tables
+ # Load actionpack sqlite3 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?
diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb
index 0a9628da8d..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="display:none">}
- 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..e220dcb8cb 100644
--- a/actionview/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -74,25 +74,77 @@ class PolymorphicRoutesTest < ActionController::TestCase
@blog_blog = Blog::Blog.new
end
+ def assert_url(url, args)
+ host = self.class.default_url_options[:host]
+
+ assert_equal url.sub(/http:\/\/#{host}/, ''), polymorphic_path(args)
+ assert_equal url, polymorphic_url(args)
+ assert_equal url, url_for(args)
+ end
+
+ def test_string
+ with_test_routes do
+ # FIXME: why are these different? Symbol case passes through to
+ # `polymorphic_url`, but the String case doesn't.
+ assert_equal "http://example.com/projects", polymorphic_url("projects")
+ assert_equal "projects", url_for("projects")
+ end
+ end
+
+ def test_string_with_options
+ with_test_routes do
+ assert_equal "http://example.com/projects?id=10", polymorphic_url("projects", :id => 10)
+ end
+ end
+
+ def test_symbol
+ with_test_routes do
+ assert_url "http://example.com/projects", :projects
+ end
+ end
+
+ def test_symbol_with_options
+ with_test_routes do
+ assert_equal "http://example.com/projects?id=10", polymorphic_url(:projects, :id => 10)
+ end
+ end
+
def test_passing_routes_proxy
with_namespaced_routes(:blog) do
proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self)
@blog_post.save
- assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([proxy, @blog_post])
+ assert_url "http://example.com/posts/#{@blog_post.id}", [proxy, @blog_post]
end
end
def test_namespaced_model
with_namespaced_routes(:blog) do
@blog_post.save
- assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url(@blog_post)
+ assert_url "http://example.com/posts/#{@blog_post.id}", @blog_post
end
end
def test_namespaced_model_with_name_the_same_as_namespace
with_namespaced_routes(:blog) do
@blog_blog.save
- assert_equal "http://example.com/blogs/#{@blog_blog.id}", polymorphic_url(@blog_blog)
+ assert_url "http://example.com/blogs/#{@blog_blog.id}", @blog_blog
+ end
+ end
+
+ def test_polymorphic_url_with_2_objects
+ with_namespaced_routes(:blog) do
+ @blog_blog.save
+ @blog_post.save
+ assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post])
+ end
+ end
+
+ def test_polymorphic_url_with_3_objects
+ with_namespaced_routes(:blog) do
+ @blog_blog.save
+ @blog_post.save
+ @fax.save
+ assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}/faxes/#{@fax.id}", polymorphic_url([@blog_blog, @blog_post, @fax])
end
end
@@ -100,41 +152,104 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_namespaced_routes(:blog) do
@blog_post.save
@blog_blog.save
- assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post])
+ assert_url "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", [@blog_blog, @blog_post]
end
end
def test_with_nil
with_test_routes do
- assert_raise ArgumentError, "Nil location provided. Can't build URI." do
+ exception = assert_raise ArgumentError do
polymorphic_url(nil)
end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_empty_list
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ polymorphic_url([])
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_nil_id
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ polymorphic_url({ :id => nil })
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_nil_in_list
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ @series.save
+ polymorphic_url([nil, @series])
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
end
end
def test_with_record
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(@project)
+ assert_url "http://example.com/projects/#{@project.id}", @project
end
end
def test_with_class
with_test_routes do
- assert_equal "http://example.com/projects", polymorphic_url(@project.class)
+ assert_url "http://example.com/projects", @project.class
+ end
+ end
+
+ def test_with_class_list_of_one
+ with_test_routes do
+ assert_url "http://example.com/projects", [@project.class]
+ end
+ end
+
+ def test_class_with_options
+ with_test_routes do
+ assert_equal "http://example.com/projects?foo=bar", polymorphic_url(@project.class, { :foo => :bar })
+ assert_equal "/projects?foo=bar", polymorphic_path(@project.class, { :foo => :bar })
end
end
def test_with_new_record
with_test_routes do
- assert_equal "http://example.com/projects", polymorphic_url(@project)
+ assert_url "http://example.com/projects", @project
+ end
+ end
+
+ def test_new_record_arguments
+ params = nil
+
+ with_test_routes do
+ extend Module.new {
+ define_method("projects_url") { |*args|
+ params = args
+ super(*args)
+ }
+
+ define_method("projects_path") { |*args|
+ params = args
+ super(*args)
+ }
+ }
+
+ assert_url "http://example.com/projects", @project
+ assert_equal [], params
end
end
def test_with_destroyed_record
with_test_routes do
@project.destroy
- assert_equal "http://example.com/projects", polymorphic_url(@project)
+ assert_url "http://example.com/projects", @project
end
end
@@ -196,14 +311,14 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_test_routes do
@project.save
@task.save
- assert_equal "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([@project, @task])
+ assert_url "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", [@project, @task]
end
end
def test_with_nested_unsaved
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task])
+ assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task]
end
end
@@ -211,20 +326,20 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_test_routes do
@project.save
@task.destroy
- assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task])
+ assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task]
end
end
def test_with_nested_class
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task.class])
+ assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task.class]
end
end
def test_class_with_array_and_namespace
with_admin_test_routes do
- assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project.class])
+ assert_url "http://example.com/admin/projects", [:admin, @project.class]
end
end
@@ -236,14 +351,14 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_unsaved_with_array_and_namespace
with_admin_test_routes do
- assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project])
+ assert_url "http://example.com/admin/projects", [:admin, @project]
end
end
def test_nested_unsaved_with_array_and_namespace
with_admin_test_routes do
@project.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/tasks", polymorphic_url([:admin, @project, @task])
+ assert_url "http://example.com/admin/projects/#{@project.id}/tasks", [:admin, @project, @task]
end
end
@@ -251,7 +366,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_admin_test_routes do
@project.save
@task.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([:admin, @project, @task])
+ assert_url "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", [:admin, @project, @task]
end
end
@@ -260,14 +375,14 @@ class PolymorphicRoutesTest < ActionController::TestCase
@project.save
@task.save
@step.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", polymorphic_url([:admin, @project, :site, @task, @step])
+ assert_url "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", [:admin, @project, :site, @task, @step]
end
end
def test_nesting_with_array_ending_in_singleton_resource
with_test_routes do
@project.save
- assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, :bid])
+ assert_url "http://example.com/projects/#{@project.id}/bid", [@project, :bid]
end
end
@@ -275,7 +390,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_test_routes do
@project.save
@task.save
- assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([@project, :bid, @task])
+ assert_url "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", [@project, :bid, @task]
end
end
@@ -291,34 +406,40 @@ class PolymorphicRoutesTest < ActionController::TestCase
with_admin_test_routes do
@project.save
@task.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([:admin, @project, :bid, @task])
+ assert_url "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", [:admin, @project, :bid, @task]
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_url "http://example.com/projects/#{@project.id}/bid", [@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_url "http://example.com/projects/#{@project.id}", [@project]
end
end
def test_with_array_containing_single_name
with_test_routes do
@project.save
- assert_equal "http://example.com/projects", polymorphic_url([:projects])
+ assert_url "http://example.com/projects", [:projects]
+ end
+ end
+
+ def test_with_array_containing_single_string_name
+ with_test_routes do
+ assert_url "http://example.com/projects", ["projects"]
end
end
def test_with_array_containing_symbols
with_test_routes do
- assert_equal "http://example.com/series/new", polymorphic_url([:new, :series])
+ assert_url "http://example.com/series/new", [:new, :series]
end
end
@@ -353,26 +474,26 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_irregular_plural_record
with_test_routes do
@tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url(@tax)
+ assert_url "http://example.com/taxes/#{@tax.id}", @tax
end
end
def test_with_irregular_plural_class
with_test_routes do
- assert_equal "http://example.com/taxes", polymorphic_url(@tax.class)
+ assert_url "http://example.com/taxes", @tax.class
end
end
def test_with_irregular_plural_new_record
with_test_routes do
- assert_equal "http://example.com/taxes", polymorphic_url(@tax)
+ assert_url "http://example.com/taxes", @tax
end
end
def test_with_irregular_plural_destroyed_record
with_test_routes do
@tax.destroy
- assert_equal "http://example.com/taxes", polymorphic_url(@tax)
+ assert_url "http://example.com/taxes", @tax
end
end
@@ -406,7 +527,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_with_nested_unsaved_irregular_plurals
with_test_routes do
@tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}/faxes", polymorphic_url([@tax, @fax])
+ assert_url "http://example.com/taxes/#{@tax.id}/faxes", [@tax, @fax]
end
end
@@ -418,34 +539,34 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_class_with_irregular_plural_array_and_namespace
with_admin_test_routes do
- assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax.class])
+ assert_url "http://example.com/admin/taxes", [:admin, @tax.class]
end
end
def test_unsaved_with_irregular_plural_array_and_namespace
with_admin_test_routes do
- assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax])
+ assert_url "http://example.com/admin/taxes", [:admin, @tax]
end
end
def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource
with_test_routes do
@tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}/bid", polymorphic_url([@tax, :bid])
+ assert_url "http://example.com/taxes/#{@tax.id}/bid", [@tax, :bid]
end
end
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_url "http://example.com/taxes/#{@tax.id}", [@tax]
end
end
def test_with_array_containing_single_name_irregular_plural
with_test_routes do
@tax.save
- assert_equal "http://example.com/taxes", polymorphic_url([:taxes])
+ assert_url "http://example.com/taxes", [:taxes]
end
end
@@ -453,15 +574,15 @@ class PolymorphicRoutesTest < ActionController::TestCase
def test_uncountable_resource
with_test_routes do
@series.save
- assert_equal "http://example.com/series/#{@series.id}", polymorphic_url(@series)
- assert_equal "http://example.com/series", polymorphic_url(Series.new)
+ assert_url "http://example.com/series/#{@series.id}", @series
+ assert_url "http://example.com/series", Series.new
end
end
def test_routing_a_to_model_delegate
with_test_routes do
@delegator.save
- assert_equal "http://example.com/model_delegates/overridden", polymorphic_url(@delegator)
+ assert_url "http://example.com/model_delegates/overridden", @delegator
end
end
@@ -470,13 +591,15 @@ class PolymorphicRoutesTest < ActionController::TestCase
set.draw do
scope(:module => name) do
resources :blogs do
- resources :posts
+ resources :posts do
+ resources :faxes
+ end
end
resources :posts
end
end
- self.class.send(:include, @routes.url_helpers)
+ extend @routes.url_helpers
yield
end
end
@@ -498,7 +621,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
resources :model_delegates
end
- self.class.send(:include, @routes.url_helpers)
+ extend @routes.url_helpers
yield
end
end
@@ -520,7 +643,7 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
- self.class.send(:include, @routes.url_helpers)
+ extend @routes.url_helpers
yield
end
end
@@ -539,8 +662,21 @@ class PolymorphicRoutesTest < ActionController::TestCase
end
end
- self.class.send(:include, @routes.url_helpers)
+ extend @routes.url_helpers
yield
end
end
end
+
+class PolymorphicPathRoutesTest < PolymorphicRoutesTest
+ include ActionView::RoutingUrlFor
+ include ActionView::Context
+
+ attr_accessor :controller
+
+ def assert_url(url, args)
+ host = self.class.default_url_options[:host]
+
+ assert_equal url.sub(/http:\/\/#{host}/, ''), url_for(args)
+ end
+end
diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb
new file mode 100644
index 0000000000..fb530b04a7
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb
@@ -0,0 +1 @@
+<%= customer_iteration_iteration.size %>-<%= customer_iteration_iteration.index %>: <%= customer_iteration.name %><%= '-first' if customer_iteration_iteration.first? %><%= '-last' if customer_iteration_iteration.last? %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb
new file mode 100644
index 0000000000..57297d0564
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb
@@ -0,0 +1 @@
+<%= client_iteration.size %>-<%= client_iteration.index %>: <%= client.name %><%= '-first' if client_iteration.first? %><%= '-last' if client_iteration.last? %> \ No newline at end of file
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/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/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index 3ca71d3376..d789a5ca27 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -195,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" />)
}
@@ -309,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")
@@ -538,6 +546,14 @@ class AssetTagHelperTest < ActionView::TestCase
assert_equal "http://cdn.example.com/images/file.png", image_path("file.png")
end
+ def test_image_url_with_asset_host_proc_returning_nil
+ @controller.config.asset_host = Proc.new { nil }
+ @controller.request = Struct.new(:base_url, :script_name).new("http://www.example.com", nil)
+
+ assert_equal "/images/rails.png", image_path("rails.png")
+ assert_equal "http://www.example.com/images/rails.png", image_url("rails.png")
+ end
+
def test_caching_image_path_with_caching_and_proc_asset_host_using_request
@controller.config.asset_host = Proc.new do |source, request|
if request.ssl?
@@ -588,6 +604,10 @@ class AssetTagHelperNonVhostTest < ActionView::TestCase
assert_equal "gopher://www.example.com", compute_asset_host("foo", :protocol => :request)
end
+ def test_should_return_custom_host_if_passed_in_options
+ assert_equal "http://custom.example.com", compute_asset_host("foo", :host => "http://custom.example.com")
+ end
+
def test_should_ignore_relative_root_path_on_complete_url
assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png"))
end
@@ -751,4 +771,15 @@ class AssetUrlHelperEmptyModuleTest < ActionView::TestCase
assert @module.config.asset_host
assert_equal "http://www.example.com/foo", @module.asset_url("foo")
end
+
+ def test_asset_url_with_custom_asset_host
+ @module.instance_eval do
+ def config
+ Struct.new(:asset_host).new("http://www.example.com")
+ end
+ end
+
+ assert @module.config.asset_host
+ assert_equal "http://custom.example.com/foo", @module.asset_url("foo", :host => "http://custom.example.com")
+ end
end
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
index 938f1c3e54..f213da5934 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -207,29 +207,6 @@ class CaptureHelperTest < ActionView::TestCase
assert_equal "", @av.with_output_buffer {}
end
- def test_flush_output_buffer_concats_output_buffer_to_response
- view = view_with_controller
- assert_equal [], view.response.body_parts
-
- view.output_buffer << 'OMG'
- view.flush_output_buffer
- assert_equal ['OMG'], view.response.body_parts
- assert_equal '', view.output_buffer
-
- view.output_buffer << 'foobar'
- view.flush_output_buffer
- assert_equal ['OMG', 'foobar'], view.response.body_parts
- assert_equal '', view.output_buffer
- end
-
- def test_flush_output_buffer_preserves_the_encoding_of_the_output_buffer
- view = view_with_controller
- alt_encoding = alt_encoding(view.output_buffer)
- view.output_buffer.force_encoding(alt_encoding)
- flush_output_buffer
- assert_equal alt_encoding, view.output_buffer.encoding
- end
-
def alt_encoding(output_buffer)
output_buffer.encoding == Encoding::US_ASCII ? Encoding::UTF_8 : Encoding::US_ASCII
end
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index 2336321f3e..b84aca6746 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -1,15 +1,8 @@
require 'abstract_unit'
class CompiledTemplatesTest < ActiveSupport::TestCase
- def setup
- # Clean up any details key cached to expose failures
- # that otherwise would appear just on isolated tests
+ teardown do
ActionView::LookupContext::DetailsKey.clear
-
- @compiled_templates = ActionView::CompiledTemplates
- @compiled_templates.instance_methods.each do |m|
- @compiled_templates.send(:remove_method, m) if m =~ /^_render_template_/
- end
end
def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index 6f77c3c99d..b86ae910c4 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -1040,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/debug_helper_test.rb b/actionview/test/template/debug_helper_test.rb
index 42d06bd9ff..5609694cd5 100644
--- a/actionview/test/template/debug_helper_test.rb
+++ b/actionview/test/template/debug_helper_test.rb
@@ -3,6 +3,6 @@ require 'active_record_unit'
class DebugHelperTest < ActionView::TestCase
def test_debug
company = Company.new(name: "firebase")
- assert_match "&nbsp; name: firebase", debug(company)
+ assert_match "name: firebase", debug(company)
end
end
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index df3a0602d1..bb375076c6 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -31,6 +31,7 @@ class DependencyTrackerTest < ActionView::TestCase
end
def teardown
+ ActionView::Template.unregister_template_handler :neckbeard
tracker.remove_tracker(:neckbeard)
end
@@ -59,6 +60,21 @@ class ERBTrackerTest < Minitest::Test
assert_equal ["messages/message123"], tracker.dependencies
end
+ def test_dependency_of_template_partial_with_layout
+ skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple!
+ template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb)
+ tracker = make_tracker("multiple/_dependencies", template)
+
+ assert_equal ["messages/layout", "messages/show"], tracker.dependencies
+ end
+
+ def test_dependency_of_template_layout_standalone
+ template = FakeTemplate.new("<%# render layout: 'messages/layout' do %>", :erb)
+ tracker = make_tracker("messages/layout", template)
+
+ assert_equal ["messages/layout"], tracker.dependencies
+ end
+
def test_finds_dependency_in_correct_directory
template = FakeTemplate.new("<%# render(message.topic) %>", :erb)
tracker = make_tracker("messages/_message", template)
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index 779a7fb53c..c2b8439df3 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -15,18 +15,30 @@ end
class FixtureFinder
FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
- attr_reader :details
+ attr_reader :details
+ attr_accessor :formats
+ attr_accessor :variants
def initialize
- @details = {}
+ @details = {}
+ @formats = []
+ @variants = []
end
def details_key
details.hash
end
- def find(logical_name, keys, partial, options)
- FixtureTemplate.new("digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb")
+ 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
@@ -88,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
@@ -194,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"])
@@ -214,6 +233,7 @@ class TemplateDigestorTest < ActionView::TestCase
assert_digest_difference("messages/edit", true) do
change_template("comments/_comment")
end
+ ensure
ActionView::Resolver.caching = resolver_before
end
@@ -242,6 +262,7 @@ class TemplateDigestorTest < ActionView::TestCase
ActionView::Resolver.caching = resolver_before
end
+
private
def assert_logged(message)
old_logger = ActionView::Base.logger
@@ -258,26 +279,33 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
- def assert_digest_difference(template_name, persistent = false)
- previous_digest = digest(template_name)
+ 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, finder, 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 finder
@finder ||= FixtureFinder.new
end
- def change_template(template_name)
- File.open("digestor/#{template_name}.html.erb", "w") do |f|
+ 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 9bacbba908..3bb84cbc50 100644
--- a/actionview/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
@@ -92,6 +92,7 @@ class ErbUtilTest < ActiveSupport::TestCase
def test_html_escape_once
assert_equal '1 &lt;&gt;&amp;&quot;&#39; 2 &amp; 3', html_escape_once('1 <>&"\' 2 &amp; 3')
+ assert_equal " &#X27; &#x27; &#x03BB; &#X03bb; &quot; &#39; &lt; &gt; ", html_escape_once(" &#X27; &#x27; &#x03BB; &#X03bb; \" ' < > ")
end
def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index 18632465db..5e991d87ad 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -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'
@@ -204,6 +221,20 @@ class FormCollectionsHelperTest < ActionView::TestCase
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
@@ -318,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 fe82349265..a9f137aec6 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -10,15 +10,20 @@ class FormHelperTest < ActionView::TestCase
@output_buffer = super
end
- def setup
- super
+ teardown do
+ I18n.backend.reload!
+ end
+ setup do
# Create "label" locale for testing I18n label helpers
I18n.backend.store_translations 'label', {
activemodel: {
attributes: {
post: {
cost: "Total cost"
+ },
+ :"post/language" => {
+ spanish: "Espanol"
}
}
},
@@ -143,75 +148,74 @@ 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_human_attribute_name_and_options
+ with_locale :label do
+ assert_dom_equal('<label for="post_language_spanish">Espanol</label>', label(:post, :language, value: "spanish"))
+ 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 +276,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>',
@@ -683,6 +694,13 @@ class FormHelperTest < ActionView::TestCase
)
end
+ def test_text_area_with_value_before_type_cast
+ assert_dom_equal(
+ %{<textarea id="post_id" name="post[id]">\n123</textarea>},
+ text_area("post", "id")
+ )
+ end
+
def test_text_area_with_html_entities
@post.body = "The HTML Entity for & is &amp;"
assert_dom_equal(
@@ -765,6 +783,22 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, date_field("post", "written_on"))
end
+ def test_date_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="2010-08-15" min="2000-06-15" name="post[written_on]" type="date" value="2004-06-15" />}
+ @post.written_on = DateTime.new(2004, 6, 15)
+ min_value = "2000-06-15"
+ max_value = "2010-08-15"
+ assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_date_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value))
+ end
+
def test_time_field
expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="00:00:00.000" />}
assert_dom_equal(expected, time_field("post", "written_on"))
@@ -800,6 +834,22 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, time_field("post", "written_on"))
end
+ def test_time_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="10:25:00.000" min="20:45:30.000" name="post[written_on]" type="time" value="01:02:03.000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "20:45:30.000"
+ max_value = "10:25:00.000"
+ assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_time_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value))
+ end
+
def test_datetime_field
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />}
assert_dom_equal(expected, datetime_field("post", "written_on"))
@@ -841,6 +891,22 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, datetime_field("post", "written_on"))
end
+ def test_datetime_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "2000-06-15T20:45:30.000+0000"
+ max_value = "2010-08-15T10:25:00.000+0000"
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_datetime_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
+
def test_datetime_local_field
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />}
assert_dom_equal(expected, datetime_local_field("post", "written_on"))
@@ -876,6 +942,22 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal(expected, datetime_local_field("post", "written_on"))
end
+ def test_datetime_local_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "2000-06-15T20:45:30"
+ max_value = "2010-08-15T10:25:00"
+ assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_datetime_local_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value))
+ end
+
def test_month_field
expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
assert_dom_equal(expected, month_field("post", "written_on"))
@@ -1427,7 +1509,7 @@ class FormHelperTest < ActionView::TestCase
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[tag_ids][]' type='hidden' value='' />"
+ "<input name='post[1][tag_ids][]' type='hidden' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1811,69 +1893,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
@@ -2405,6 +2479,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 = []
@@ -3023,12 +3109,13 @@ class FormHelperTest < ActionView::TestCase
protected
def hidden_fields(method = nil)
- txt = %{<div style="display:none">}
- 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)
@@ -3052,4 +3139,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 50e9d132a7..d25fa3706f 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -119,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>",
@@ -571,6 +591,19 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_select_under_fields_for_with_block_without_options
+ @post = Post.new
+
+ output_buffer = fields_for :post, @post do |f|
+ concat(f.select(:category) {})
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"></select>",
+ output_buffer
+ )
+ end
+
def test_select_with_multiple_to_add_hidden_input
output_buffer = select(:post, :category, "", {}, :multiple => true)
assert_dom_equal(
@@ -813,7 +846,7 @@ class FormOptionsHelperTest < ActionView::TestCase
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 0d5831dc6f..18c739674a 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -14,12 +14,15 @@ class FormTagHelperTest < ActionView::TestCase
method = options[:method]
enforce_utf8 = options.fetch(:enforce_utf8, true)
- txt = %{<div style="display:none">}
- txt << %{<input name="utf8" type="hidden" value="&#x2713;" />} if enforce_utf8
- 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 = {})
@@ -476,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 4703111741..9ba7f64ad1 100644
--- a/actionview/test/template/javascript_helper_test.rb
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -12,14 +12,14 @@ class JavaScriptHelperTest < ActionView::TestCase
yield if block_given?
end
- def setup
- super
+ setup do
+ @old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json
ActiveSupport.escape_html_entities_in_json = true
@template = self
end
def teardown
- ActiveSupport.escape_html_entities_in_json = false
+ ActiveSupport.escape_html_entities_in_json = @old_escape_html_entities_in_json
end
def test_escape_javascript
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index ce9485e146..4f7823045e 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -93,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]
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
index be336ea3fb..b59883b760 100644
--- a/actionview/test/template/number_helper_test.rb
+++ b/actionview/test/template/number_helper_test.rb
@@ -8,6 +8,8 @@ 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
@@ -16,14 +18,23 @@ class NumberHelperTest < ActionView::TestCase
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: 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
@@ -37,6 +48,7 @@ class NumberHelperTest < ActionView::TestCase
assert_equal "-111.235", number_with_precision(-111.2346)
assert_equal "111.00", number_with_precision(111, precision: 2)
assert_equal "0.00100", number_with_precision(0.001, precision: 5)
+ assert_equal "3.33", number_with_precision(Rational(10, 3), precision: 2)
end
def test_number_to_human_size
@@ -52,6 +64,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>")
@@ -73,6 +110,14 @@ 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)
+ ensure
+ I18n.reload!
+ 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/output_buffer_test.rb b/actionview/test/template/output_buffer_test.rb
deleted file mode 100644
index eb0df3d1ab..0000000000
--- a/actionview/test/template/output_buffer_test.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-require 'abstract_unit'
-
-class OutputBufferTest < ActionController::TestCase
- class TestController < ActionController::Base
- def index
- render :text => 'foo'
- end
- end
-
- tests TestController
-
- def setup
- @vc = @controller.view_context
- get :index
- assert_equal ['foo'], body_parts
- end
-
- test 'output buffer is nil after rendering' do
- assert_nil output_buffer
- end
-
- test 'flushing ignores nil output buffer' do
- @controller.view_context.flush_output_buffer
- assert_nil output_buffer
- assert_equal ['foo'], body_parts
- end
-
- test 'flushing ignores empty output buffer' do
- @vc.output_buffer = ''
- @vc.flush_output_buffer
- assert_equal '', output_buffer
- assert_equal ['foo'], body_parts
- end
-
- test 'flushing appends the output buffer to the body parts' do
- @vc.output_buffer = 'bar'
- @vc.flush_output_buffer
- assert_equal '', output_buffer
- assert_equal ['foo', 'bar'], body_parts
- end
-
- test 'flushing preserves output buffer encoding' do
- original_buffer = ' '.force_encoding(Encoding::EUC_JP)
- @vc.output_buffer = original_buffer
- @vc.flush_output_buffer
- assert_equal ['foo', original_buffer], body_parts
- assert_not_equal original_buffer, output_buffer
- assert_equal Encoding::EUC_JP, output_buffer.encoding
- end
-
- protected
- def output_buffer
- @vc.output_buffer
- end
-
- def body_parts
- @controller.response.body_parts
- end
-end
diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
index 76c71c9e6d..a1bf0e1a5f 100644
--- a/actionview/test/template/output_safety_helper_test.rb
+++ b/actionview/test/template/output_safety_helper_test.rb
@@ -25,4 +25,11 @@ class OutputSafetyHelperTest < ActionView::TestCase
assert_equal "<p>foo</p><br /><p>bar</p>", joined
end
-end \ No newline at end of file
+ test "safe_join should work recursively similarly to Array.join" do
+ joined = safe_join(['a',['b','c']], ':')
+ assert_equal 'a:b:c', joined
+
+ joined = safe_join(['"a"',['<b>','<c>']], ' <br/> ')
+ assert_equal '&quot;a&quot; &lt;br/&gt; &lt;b&gt; &lt;br/&gt; &lt;c&gt;', joined
+ end
+end
diff --git a/actionview/test/template/partial_iteration_test.rb b/actionview/test/template/partial_iteration_test.rb
new file mode 100644
index 0000000000..695f9f1bef
--- /dev/null
+++ b/actionview/test/template/partial_iteration_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'action_view/renderer/partial_renderer'
+
+class PartialIterationTest < ActiveSupport::TestCase
+ def test_has_size_and_index
+ iteration = ActionView::PartialIteration.new 3
+ assert_equal 0, iteration.index, "should be at the first index"
+ assert_equal 3, iteration.size, "should have the size"
+ end
+
+ def test_first_is_true_when_current_is_at_the_first_index
+ iteration = ActionView::PartialIteration.new 3
+ assert iteration.first?, "first when current is 0"
+ end
+
+ def test_first_is_false_unless_current_is_at_the_first_index
+ iteration = ActionView::PartialIteration.new 3
+ iteration.iterate!
+ assert !iteration.first?, "not first when current is 1"
+ end
+
+ def test_last_is_true_when_current_is_at_the_last_index
+ iteration = ActionView::PartialIteration.new 3
+ iteration.iterate!
+ iteration.iterate!
+ assert iteration.last?, "last when current is 2"
+ end
+
+ def test_last_is_false_unless_current_is_at_the_last_index
+ iteration = ActionView::PartialIteration.new 3
+ assert !iteration.last?, "not last when current is 0"
+ end
+end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index db5d99755c..85817119ba 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -12,7 +12,6 @@ module RenderTestCases
@controller_view = TestController.new.view_context
# Reload and register danish language for testing
- I18n.reload!
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
@@ -22,7 +21,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
@@ -257,7 +256,7 @@ module RenderTestCases
end
def test_render_partial_collection_without_as
- assert_equal "local_inspector,local_inspector_counter",
+ assert_equal "local_inspector,local_inspector_counter,local_inspector_iteration",
@view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ])
end
@@ -325,11 +324,16 @@ module RenderTestCases
@controller_view.render(customers, :greeting => "Hello")
end
+ def test_render_partial_using_collection_without_path
+ assert_equal "hi good customer: david0", @controller_view.render([ GoodCustomer.new("david") ], greeting: "hi")
+ end
+
def test_render_partial_without_object_or_collection_does_not_generate_partial_name_local_variable
exception = assert_raises ActionView::Template::Error do
@controller_view.render("partial_name_local_variable")
end
- assert_match "undefined local variable or method `partial_name_local_variable'", exception.message
+ assert_instance_of NameError, exception.original_exception
+ assert_equal :partial_name_local_variable, exception.original_exception.name
end
# TODO: The reason for this test is unclear, improve documentation
@@ -369,23 +373,48 @@ module RenderTestCases
def test_render_inline_with_render_from_to_proc
ActionView::Template.register_template_handler :ruby_handler, :source.to_proc
- assert_equal '3', @view.render(:inline => "(1 + 2).to_s", :type => :ruby_handler)
+ assert_equal '3', @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ ensure
+ ActionView::Template.unregister_template_handler :ruby_handler
end
def test_render_inline_with_compilable_custom_type
ActionView::Template.register_template_handler :foo, CustomHandler
- assert_equal 'source: "Hello, World!"', @view.render(:inline => "Hello, World!", :type => :foo)
+ assert_equal 'source: "Hello, World!"', @view.render(inline: "Hello, World!", type: :foo)
+ ensure
+ ActionView::Template.unregister_template_handler :foo
end
def test_render_inline_with_locals_and_compilable_custom_type
ActionView::Template.register_template_handler :foo, CustomHandler
- assert_equal 'source: "Hello, <%= name %>!"', @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" }, :type => :foo)
+ assert_equal 'source: "Hello, <%= name %>!"', @view.render(inline: "Hello, <%= name %>!", locals: { name: "Josh" }, type: :foo)
+ ensure
+ ActionView::Template.unregister_template_handler :foo
+ end
+
+ def test_render_body
+ assert_equal 'some body', @view.render(body: 'some body')
+ end
+
+ def test_render_plain
+ assert_equal 'some plaintext', @view.render(plain: 'some plaintext')
end
def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization
ActionView::Template::Handlers.extensions
ActionView::Template.register_template_handler :foo, CustomHandler
assert ActionView::Template::Handlers.extensions.include?(:foo)
+ ensure
+ ActionView::Template.unregister_template_handler :foo
+ end
+
+ def test_render_does_not_use_unregistered_extension_and_template_handler
+ ActionView::Template.register_template_handler :foo, CustomHandler
+ ActionView::Template.unregister_template_handler :foo
+ assert_not ActionView::Template::Handlers.extensions.include?(:foo)
+ assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :foo)
+ ensure
+ ActionView::Template::Handlers.class_variable_get(:@@template_handlers).delete(:foo)
end
def test_render_ignores_templates_with_malformed_template_handlers
@@ -474,7 +503,9 @@ module RenderTestCases
def test_render_with_passing_couple_extensions_to_one_register_template_handler_function_call
ActionView::Template.register_template_handler :foo1, :foo2, CustomHandler
- assert_equal @view.render(:inline => "Hello, World!", :type => :foo1), @view.render(:inline => "Hello, World!", :type => :foo2)
+ assert_equal @view.render(inline: "Hello, World!", type: :foo1), @view.render(inline: "Hello, World!", type: :foo2)
+ ensure
+ ActionView::Template.unregister_template_handler :foo1, :foo2
end
def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call
@@ -494,6 +525,7 @@ class CachedViewRenderTest < ActiveSupport::TestCase
def teardown
GC.start
+ I18n.reload!
end
end
@@ -511,6 +543,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase
def teardown
GC.start
+ I18n.reload!
end
def test_render_utf8_template_with_magic_comment
diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index 12d5260a9d..f7c8f36b78 100644
--- a/actionview/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -1,6 +1,6 @@
require 'abstract_unit'
-# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
+# The exhaustive tests are in test/template/html-scanner/sanitizer_test.rb
# This tests the that the helpers hook up correctly to the sanitizer classes.
class SanitizeHelperTest < ActionView::TestCase
tests ActionView::Helpers::SanitizeHelper
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
index fb016a52de..0ea669b3d0 100644
--- a/actionview/test/template/tag_helper_test.rb
+++ b/actionview/test/template/tag_helper_test.rb
@@ -80,11 +80,27 @@ class TagHelperTest < ActionView::TestCase
str = content_tag('p', "limelight", :class => ["song", "play"])
assert_equal "<p class=\"song play\">limelight</p>", str
+
+ str = content_tag('p', "limelight", :class => ["song", ["play"]])
+ assert_equal "<p class=\"song play\">limelight</p>", str
end
def test_content_tag_with_unescaped_array_class
str = content_tag('p', "limelight", {:class => ["song", "play>"]}, false)
assert_equal "<p class=\"song play>\">limelight</p>", str
+
+ str = content_tag('p', "limelight", {:class => ["song", ["play>"]]}, false)
+ assert_equal "<p class=\"song play>\">limelight</p>", str
+ end
+
+ def test_content_tag_with_empty_array_class
+ str = content_tag('p', 'limelight', {:class => []})
+ assert_equal '<p class="">limelight</p>', str
+ end
+
+ def test_content_tag_with_unescaped_empty_array_class
+ str = content_tag('p', 'limelight', {:class => []}, false)
+ assert_equal '<p class="">limelight</p>', str
end
def test_content_tag_with_data_attributes
@@ -107,6 +123,7 @@ class TagHelperTest < ActionView::TestCase
def test_escape_once
assert_equal '1 &lt; 2 &amp; 3', escape_once('1 < 2 &amp; 3')
+ assert_equal " &#X27; &#x27; &#x03BB; &#X03bb; &quot; &#39; &lt; &gt; ", escape_once(" &#X27; &#x27; &#x03BB; &#X03bb; \" ' < > ")
end
def test_tag_honors_html_safe_for_param_values
@@ -115,6 +132,14 @@ class TagHelperTest < ActionView::TestCase
end
end
+ def test_tag_honors_html_safe_with_escaped_array_class
+ str = tag('p', :class => ['song>', 'play>'.html_safe])
+ assert_equal '<p class="song&gt; play>" />', str
+
+ str = tag('p', :class => ['song>'.html_safe, 'play>'])
+ assert_equal '<p class="song> play&gt;" />', str
+ end
+
def test_skip_invalid_escaped_attributes
['&1;', '&#1dfa3;', '& #123;'].each do |escaped|
assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}" />), tag('a', :href => escaped)
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 4ee0930341..4582fa13ee 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'rails/engine'
module ActionView
@@ -223,7 +224,7 @@ module ActionView
test "is able to use mounted routes" do
with_routing do |set|
- app = Class.new do
+ app = Class.new(Rails::Engine) do
def self.routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb
index 108a674d95..88bac85039 100644
--- a/actionview/test/template/test_test.rb
+++ b/actionview/test/template/test_test.rb
@@ -37,10 +37,22 @@ 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 to_model; self; end
+ def persisted?; true; end
+ 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/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index a514bba83d..db416a8de4 100644
--- a/actionview/test/template/text_helper_test.rb
+++ b/actionview/test/template/text_helper_test.rb
@@ -222,6 +222,11 @@ class TextHelperTest < ActionView::TestCase
)
end
+ def test_highlight_accepts_regexp
+ assert_equal("This day was challenging for judge <mark>Allen</mark> and his colleagues.",
+ highlight("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i))
+ end
+
def test_highlight_with_multiple_phrases_in_one_pass
assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), :highlighter => '<em>\1</em>')
end
@@ -260,6 +265,13 @@ class TextHelperTest < ActionView::TestCase
assert_equal options, passed_options
end
+ def test_highlight_with_block
+ assert_equal(
+ "<b>one</b> <b>two</b> <b>three</b>",
+ highlight("one two three", ["one", "two", "three"]) { |word| "<b>#{word}</b>" }
+ )
+ end
+
def test_excerpt
assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5))
assert_equal("This is a...", excerpt("This is a beautiful morning", "this", :radius => 5))
@@ -267,6 +279,16 @@ class TextHelperTest < ActionView::TestCase
assert_nil excerpt("This is a beautiful morning", "day")
end
+ def test_excerpt_with_regex
+ assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\bbeau\w*\b/i, :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\b(beau\w*)\b/i, :radius => 5))
+ assert_equal("...udge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 5))
+ assert_equal("...judge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 1, :separator => ' '))
+ assert_equal("...was challenging for...", excerpt("This day was challenging for judge Allen and his colleagues.", /\b(\w*allen\w*)\b/i, :radius => 5))
+ end
+
def test_excerpt_should_not_be_html_safe
assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe?
end
@@ -288,11 +310,6 @@ class TextHelperTest < ActionView::TestCase
assert_equal("...abc...", excerpt("z abc d", "b", :radius => 1))
end
- def test_excerpt_with_regex
- assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5))
- assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5))
- end
-
def test_excerpt_with_omission
assert_equal("[...]is a beautiful morn[...]", excerpt("This is a beautiful morning", "beautiful", :omission => "[...]",:radius => 5))
assert_equal(
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 c4770840fb..41f6770f23 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -6,7 +6,7 @@ class TranslationHelperTest < ActiveSupport::TestCase
attr_reader :request, :view
- def setup
+ setup do
I18n.backend.store_translations(:en,
:translations => {
:templates => {
@@ -30,6 +30,10 @@ class TranslationHelperTest < ActiveSupport::TestCase
@view = ::ActionView::Base.new(ActionController::Base.view_paths, {})
end
+ teardown do
+ I18n.backend.reload!
+ end
+
def test_delegates_to_i18n_setting_the_rescue_format_option_to_html
I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("")
translate :foo, :locale => 'en'
@@ -151,4 +155,10 @@ class TranslationHelperTest < ActiveSupport::TestCase
translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string'])
assert_equal 'A Generic String', translation
end
+
+ def test_translate_does_not_change_options
+ options = {}
+ translate(:'translations.missing', options)
+ assert_equal({}, options)
+ end
end
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 7e978e15d2..35279a4558 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -53,12 +53,12 @@ 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"><div><input type="submit" value="Hello" /></div></form>},
+ %{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>},
button_to("Hello", article_path("Hello".html_safe))
)
end
@@ -67,7 +67,7 @@ class UrlHelperTest < ActiveSupport::TestCase
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
@@ -75,102 +75,102 @@ 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"><div><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></div></form>},
+ %{<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
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 500d8dc42f..c14b0688c7 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,72 +1,58 @@
-* `#to_param` returns `nil` if `#to_key` returns `nil`. Fixes #11399.
+* Passwords with spaces only allowed in `ActiveModel::SecurePassword`.
- *Yves Senn*
+ Presence validation can be used to restore old behavior.
-* Ability to specify multiple contexts when defining a validation.
+ *Yevhene Shemet*
- Example:
+* Validate options passed to `ActiveModel::Validations.validate`.
- class Person
- include ActiveModel::Validations
+ Preventing, in many cases, the simple mistake of using `validate` instead of `validates`.
- attr_reader :name
- validates_presence_of :name, on: [:verify, :approve]
- end
+ *Sonny Michaud*
- person = Person.new
- person.valid? # => true
- person.valid?(:verify) # => false
- person.errors.full_messages_for(:name) # => ["Name can't be blank"]
- person.valid?(:approve) # => false
- person.errors.full_messages_for(:name) # => ["Name can't be blank"]
+* Deprecate `reset_#{attribute}` in favor of `restore_#{attribute}`.
- *Vince Puzzella*
+ These methods may cause confusion with the `reset_changes` that behaves differently
+ of them.
-* `attribute_changed?` now accepts a hash to check if the attribute was
- changed `:from` and/or `:to` a given value.
+* Deprecate `ActiveModel::Dirty#reset_changes` in favor of `#clear_changes_information`.
- Example:
+ This method name is causing confusion with the `reset_#{attribute}`
+ methods. While `reset_name` set the value of the name attribute for the
+ previous value `reset_changes` only discard the changes and previous
+ changes.
- model.name_changed?(from: "Pete", to: "Ringo")
+* Added `restore_attributes` method to `ActiveModel::Dirty` API to restore all the
+ changed values to the previous data.
- *Tejas Dinkar*
+ *Igor G.*
-* Fix `has_secure_password` to honor bcrypt-ruby's cost attribute.
+* Allow proc and symbol as values for `only_integer` of `NumericalityValidator`
- *T.J. Schuck*
+ *Robin Mehner*
-* Updated the `ActiveModel::Dirty#changed_attributes` method to be indifferent between using
- symbols and strings as keys.
+* `has_secure_password` now verifies that the given password is less than 72
+ characters if validations are enabled.
- *William Myers*
+ Fixes #14591.
-* Added new API methods `reset_changes` and `changes_applied` to `ActiveModel::Dirty`
- that control changes state. Previsously you needed to update internal
- instance variables, but now API methods are available.
+ *Akshay Vishnoi*
- *Bogdan Gusiev*
+* Remove deprecated `Validator#setup` without replacement.
-* Fix `has_secure_password` not to trigger `password_confirmation` validations
- if no `password_confirmation` is set.
+ See #10716.
- *Vladimir Kiselev*
+ *Kuldeep Aggarwal*
-* `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/README.rdoc b/activemodel/README.rdoc
index 4c00755532..f6beff14e1 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']}
@@ -116,7 +132,10 @@ behavior out of the box:
"Name"
end
end
-
+
+ person = Person.new
+ person.name = nil
+ person.validate!
person.errors.full_messages
# => ["Name cannot be nil"]
@@ -128,7 +147,7 @@ behavior out of the box:
extend ActiveModel::Naming
end
- NamedPerson.model_name # => "NamedPerson"
+ NamedPerson.model_name.name # => "NamedPerson"
NamedPerson.model_name.human # => "Named person"
{Learn more}[link:classes/ActiveModel/Naming.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 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
- 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]
@@ -243,6 +262,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 3e49c34182..feb3d9371d 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -48,6 +48,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
+ autoload :StrictValidationFailed, 'active_model/errors'
end
module Serializers
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 0a19ef686d..9c9b6f4a77 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -40,13 +40,15 @@ module ActiveModel
self
end
- # Returns an Enumerable of all key attributes if any is set, regardless if
+ # Returns an Array of all key attributes if any is set, regardless if
# the object is persisted or not. Returns +nil+ if there are no key attributes.
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Conversion
+ # attr_accessor :id
# end
#
- # person = Person.create
+ # person = Person.create(id: 1)
# person.to_key # => [1]
def to_key
key = respond_to?(:id) && id
@@ -56,10 +58,15 @@ module ActiveModel
# Returns a +string+ representing the object's key suitable for use in URLs,
# or +nil+ if <tt>persisted?</tt> is +false+.
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Conversion
+ # attr_accessor :id
+ # def persisted?
+ # true
+ # end
# end
#
- # person = Person.create
+ # person = Person.create(id: 1)
# person.to_param # => "1"
def to_param
(persisted? && key = to_key) ? key.join('-') : nil
@@ -83,8 +90,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 98ffffeb10..d11243c4c0 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -15,8 +15,9 @@ module ActiveModel
# * Call <tt>attr_name_will_change!</tt> before each change to the tracked
# attribute.
# * Call <tt>changes_applied</tt> after the changes are persisted.
- # * Call <tt>reset_changes</tt> when you want to reset the changes
+ # * Call <tt>clear_changes_information</tt> when you want to reset the changes
# information.
+ # * Call <tt>restore_attributes</tt> when you want to restore previous data.
#
# A minimal implementation could be:
#
@@ -36,11 +37,18 @@ module ActiveModel
#
# def save
# # do persistence work
+ #
# changes_applied
# end
#
# def reload!
- # reset_changes
+ # # get the values from the persistence layer
+ #
+ # clear_changes_information
+ # end
+ #
+ # def rollback!
+ # restore_attributes
# end
# end
#
@@ -72,6 +80,13 @@ module ActiveModel
# person.reload!
# person.previous_changes # => {}
#
+ # Rollback the changes:
+ #
+ # person.name = "Uncle Bob"
+ # person.rollback!
+ # person.name # => "Bill"
+ # person.name_changed? # => false
+ #
# Assigning the same value leaves the attribute unchanged:
#
# person.name = 'Bill'
@@ -84,9 +99,11 @@ module ActiveModel
# person.changed # => ["name"]
# person.changes # => {"name" => ["Bill", "Bob"]}
#
- # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt>
- # to mark that the attribute is changing. Otherwise ActiveModel can't track
- # changes to in-place attributes.
+ # If an attribute is modified in-place then make use of
+ # +[attribute_name]_will_change!+ to mark that the attribute is changing.
+ # Otherwise Active Model can't track changes to in-place attributes. Note
+ # that Active Record can detect in-place modifications automatically. You do
+ # not need to call +[attribute_name]_will_change!+ on Active Record models.
#
# person.name_will_change!
# person.name_change # => ["Bill", "Bill"]
@@ -99,6 +116,7 @@ module ActiveModel
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
attribute_method_affix prefix: 'reset_', suffix: '!'
+ attribute_method_affix prefix: 'restore_', suffix: '!'
end
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
@@ -162,20 +180,30 @@ module ActiveModel
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
+ # Restore all previous data of the provided attributes.
+ def restore_attributes(attributes = changed)
+ attributes.each { |attr| restore_attribute! attr }
+ end
+
private
# Removes current changes and makes them accessible through +previous_changes+.
- def changes_applied
+ def changes_applied # :doc:
@previously_changed = changes
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
end
- # Removes all dirty data: current changes and previous changes
- def reset_changes
+ # Clear all dirty data: current changes and previous changes.
+ def clear_changes_information # :doc:
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
end
+ def reset_changes
+ ActiveSupport::Deprecation.warn "#reset_changes is deprecated and will be removed on Rails 5. Please use #clear_changes_information instead."
+ clear_changes_information
+ end
+
# Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
@@ -196,6 +224,13 @@ module ActiveModel
# Handle <tt>reset_*!</tt> for +method_missing+.
def reset_attribute!(attr)
+ ActiveSupport::Deprecation.warn "#reset_#{attr}! is deprecated and will be removed on Rails 5. Please use #restore_#{attr}! instead."
+
+ restore_attribute!(attr)
+ end
+
+ # Handle <tt>restore_*!</tt> for +method_missing+.
+ def restore_attribute!(attr)
if attribute_changed?(attr)
__send__("#{attr}=", changed_attributes[attr])
changed_attributes.delete(attr)
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 9c3bc913e1..1d025beeef 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -23,7 +23,7 @@ module ActiveModel
# attr_reader :errors
#
# def validate!
- # errors.add(:name, "cannot be nil") if name == nil
+ # errors.add(:name, "cannot be nil") if name.nil?
# end
#
# # The following methods are needed to be minimally implemented
@@ -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/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 63716eebb1..640024eaa1 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -16,8 +16,8 @@ module ActiveModel
# end
#
# person = Person.new(name: 'bob', age: '18')
- # person.name # => 'bob'
- # person.age # => 18
+ # person.name # => "bob"
+ # person.age # => "18"
#
# Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
# to return +false+, which is the most common case. You may want to override
@@ -74,7 +74,7 @@ module ActiveModel
#
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
- # person.age # => 18
+ # person.age # => "18"
def initialize(params={})
params.each do |attr, value|
self.public_send("#{attr}=", value)
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 11ebfe6cc0..241e88deeb 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -204,7 +204,7 @@ module ActiveModel
# extend ActiveModel::Naming
# end
#
- # BookCover.model_name # => "BookCover"
+ # BookCover.model_name.name # => "BookCover"
# BookCover.model_name.human # => "Book cover"
#
# BookCover.model_name.i18n_key # => :book_cover
@@ -214,14 +214,20 @@ module ActiveModel
# is required to pass the Active Model Lint test. So either extending the
# provided method below, or rolling your own is required.
module Naming
+ def self.extended(base) #:nodoc:
+ base.remove_possible_method :model_name
+ base.delegate :model_name, to: :class
+ end
+
# Returns an ActiveModel::Name object for module. It can be
# used to retrieve all kinds of naming-related information
# (See ActiveModel::Name for more information).
#
- # class Person < ActiveModel::Model
+ # class Person
+ # include ActiveModel::Model
# end
#
- # Person.model_name # => Person
+ # Person.model_name.name # => "Person"
# Person.model_name.class # => ActiveModel::Name
# Person.model_name.singular # => "person"
# Person.model_name.plural # => "people"
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 01739d8ae4..f6ad35769f 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -2,6 +2,11 @@ module ActiveModel
module SecurePassword
extend ActiveSupport::Concern
+ # BCrypt hash function can handle maximum 72 characters, and if we pass
+ # password of length more than 72 characters it ignores extra characters.
+ # Hence need to put a restriction on password length.
+ MAX_PASSWORD_LENGTH_ALLOWED = 72
+
class << self
attr_accessor :min_cost # :nodoc:
end
@@ -11,18 +16,22 @@ module ActiveModel
# Adds methods to set and authenticate against a BCrypt password.
# 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
- # you wish to turn off validations, pass <tt>validations: false</tt> as an
- # argument. You can add more validations by hand if need be.
+ # The following validations are added automatically:
+ # * Password must be present on creation
+ # * Password length should be less than or equal to 72 characters
+ # * Confirmation of password (using a +password_confirmation+ attribute)
+ #
+ # If password confirmation validation is not needed, simply leave out the
+ # value for +password_confirmation+ (i.e. don't provide a form field for
+ # it). When this attribute has a +nil+ value, the validation will not be
+ # triggered.
#
- # If you don't need the confirmation validation, just don't set any
- # value to the password_confirmation attribute and the validation
- # will not be triggered.
+ # For further customizability, it is possible to supress the default
+ # validations by passing <tt>validations: false</tt> as an argument.
#
- # You need to add bcrypt-ruby (~> 3.1.2) to Gemfile to use #has_secure_password:
+ # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
#
- # gem 'bcrypt-ruby', '~> 3.1.2'
+ # gem 'bcrypt', '~> 3.1.7'
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
@@ -42,21 +51,21 @@ 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
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
- attr_reader :password
-
include InstanceMethodsOnActivation
if options.fetch(:validations, true)
+ include ActiveModel::Validations
+
# 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
@@ -65,9 +74,11 @@ module ActiveModel
record.errors.add(:password, :blank) unless record.password_digest.present?
end
+ validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of :password, if: ->{ password.present? }
end
+ # This code is necessary as long as the protected_attributes gem is supported.
if respond_to?(:attributes_protected_by_default)
def self.attributes_protected_by_default #:nodoc:
super + ['password_digest']
@@ -91,8 +102,10 @@ module ActiveModel
BCrypt::Password.new(password_digest) == unencrypted_password && self
end
+ attr_reader :password
+
# Encrypts the password into the +password_digest+ attribute, only if the
- # new password is not blank.
+ # new password is not empty.
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
@@ -106,7 +119,7 @@ module ActiveModel
def password=(unencrypted_password)
if unencrypted_password.nil?
self.password_digest = nil
- elsif unencrypted_password.present?
+ elsif !unencrypted_password.empty?
@password = unencrypted_password
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 36a6c00290..976f50b13e 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/slice'
module ActiveModel
# == Active \Model \Serialization
#
- # Provides a basic serialization to a serializable_hash for your object.
+ # Provides a basic serialization to a serializable_hash for your objects.
#
# A minimal implementation could be:
#
@@ -25,14 +25,14 @@ module ActiveModel
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
#
- # You need to declare an attributes hash which contains the attributes you
- # want to serialize. Attributes must be strings, not symbols. When called,
- # serializable hash will use instance methods that match the name of the
- # attributes hash's keys. In order to override this behavior, take a look at
- # the private method +read_attribute_for_serialization+.
+ # An +attributes+ hash must be defined and should contain any attributes you
+ # need to be serialized. Attributes must be strings, not symbols.
+ # When called, serializable hash will use instance methods that match the name
+ # of the attributes hash's keys. In order to override this behavior, take a look
+ # at the private method +read_attribute_for_serialization+.
#
- # Most of the time though, you will want to include the JSON or XML
- # serializations. Both of these modules automatically include the
+ # Most of the time though, either the JSON or XML serializations are needed.
+ # Both of these modules automatically include the
# <tt>ActiveModel::Serialization</tt> module, so there is no need to
# explicitly include it.
#
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index e9674d5143..f67a3be5c1 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -141,6 +141,11 @@ module ActiveModel
# value.
def validate(*args, &block)
options = args.extract_options!
+
+ if args.all? { |arg| arg.is_a?(Symbol) }
+ options.assert_valid_keys([:on, :if, :unless])
+ end
+
if options.key?(:on)
options = options.dup
options[:if] = Array(options[:if])
@@ -148,6 +153,7 @@ module ActiveModel
Array(options[:on]).include?(o.validation_context)
}
end
+
args << options
set_callback(:validate, *args, &block)
end
@@ -285,6 +291,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
#
@@ -319,6 +327,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/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index a9fb9804d4..5bd162433d 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -30,7 +30,7 @@ module ActiveModel
return
end
- if options[:only_integer]
+ if allow_only_integer?(record)
unless value = parse_raw_value_as_an_integer(raw_value)
record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
return
@@ -75,6 +75,17 @@ module ActiveModel
filtered[:value] = value
filtered
end
+
+ def allow_only_integer?(record)
+ case options[:only_integer]
+ when Symbol
+ record.send(options[:only_integer])
+ when Proc
+ options[:only_integer].call(record)
+ else
+ options[:only_integer]
+ end
+ end
end
module HelperMethods
@@ -121,6 +132,7 @@ module ActiveModel
# * <tt>:equal_to</tt>
# * <tt>:less_than</tt>
# * <tt>:less_than_or_equal_to</tt>
+ # * <tt>:only_integer</tt>
#
# For example:
#
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 bddacc8c45..0116de68ab 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -79,7 +79,7 @@ module ActiveModel
# include ActiveModel::Validations
# attr_accessor :title
#
- # validates :title, presence: true
+ # validates :title, presence: true, title: true
# end
#
# It can be useful to access the class that is using that validator when there are prerequisites such
@@ -106,7 +106,6 @@ module ActiveModel
# Accepts options that will be made available through the +options+ reader.
def initialize(options = {})
@options = options.except(:class).freeze
- deprecated_setup(options)
end
# Returns the kind for this validator.
@@ -122,21 +121,6 @@ module ActiveModel
def validate(record)
raise NotImplementedError, "Subclasses must implement a validate(record) method."
end
-
- private
- def deprecated_setup(options) # TODO: remove me in 4.2.
- return unless respond_to?(:setup)
- ActiveSupport::Deprecation.warn "The `Validator#setup` instance method is deprecated and will be removed on Rails 4.2. Do your setup in the constructor instead:
-
-class MyValidator < ActiveModel::Validator
- def initialize(options={})
- super
- options[:class].send :attr_accessor, :custom_attribute
- end
-end
-"
- setup(options[:class])
- end
end
# +EachValidator+ is a validator which iterates through the attributes given
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index 58ba3ab9b2..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.beta1"
- 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 c5cfbf909d..800cad6d9a 100644
--- a/activemodel/test/cases/conversion_test.rb
+++ b/activemodel/test/cases/conversion_test.rb
@@ -24,6 +24,10 @@ class ConversionTest < ActiveModel::TestCase
assert_equal "1", Contact.new(id: 1).to_param
end
+ test "to_param returns the string joined by '-'" do
+ assert_equal "abc-xyz", Contact.new(id: ["abc", "xyz"]).to_param
+ end
+
test "to_param returns nil if to_key is nil" do
klass = Class.new(Contact) do
def persisted?
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index 2853476c91..db2cd885e2 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -43,6 +43,10 @@ class DirtyTest < ActiveModel::TestCase
end
def reload
+ clear_changes_information
+ end
+
+ def deprecated_reload
reset_changes
end
end
@@ -107,7 +111,7 @@ class DirtyTest < ActiveModel::TestCase
test "resetting attribute" do
@model.name = "Bob"
- @model.reset_name!
+ @model.restore_name!
assert_nil @model.name
assert !@model.name_changed?
end
@@ -176,4 +180,49 @@ class DirtyTest < ActiveModel::TestCase
assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
end
+
+ test "reset_changes is deprecated" 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']
+
+ assert_deprecated do
+ @model.deprecated_reload
+ end
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "restore_attributes should restore all previous data" do
+ @model.name = 'Dmitry'
+ @model.color = 'Red'
+ @model.save
+ @model.name = 'Bob'
+ @model.color = 'White'
+
+ @model.restore_attributes
+
+ assert_not @model.changed?
+ assert_equal 'Dmitry', @model.name
+ assert_equal 'Red', @model.color
+ end
+
+ test "restore_attributes can restore only some attributes" do
+ @model.name = 'Dmitry'
+ @model.color = 'Red'
+ @model.save
+ @model.name = 'Bob'
+ @model.color = 'White'
+
+ @model.restore_attributes(['name'])
+
+ assert @model.changed?
+ assert_equal 'Dmitry', @model.name
+ assert_equal 'White', @model.color
+ end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index def28578f8..42d0365521 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -82,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")
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 522a7cebb4..804e0c24f6 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -11,3 +11,5 @@ ActiveSupport::Deprecation.debug = true
I18n.enforce_available_locales = false
require 'active_support/testing/autorun'
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb
index aa683f4152..7b8287edbf 100644
--- a/activemodel/test/cases/naming_test.rb
+++ b/activemodel/test/cases/naming_test.rb
@@ -272,3 +272,9 @@ class NameWithAnonymousClassTest < ActiveModel::TestCase
assert_equal "Anonymous", model_name
end
end
+
+class NamingMethodDelegationTest < ActiveModel::TestCase
+ def test_model_name
+ assert_equal Blog::Post.model_name, Blog::Post.new.model_name
+ end
+end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 82fd291064..6d56c8344a 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -4,6 +4,8 @@ require 'models/visitor'
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
@@ -15,18 +17,32 @@ class SecurePasswordTest < ActiveModel::TestCase
end
teardown do
- ActiveModel::SecurePassword.min_cost = false
+ ActiveModel::SecurePassword.min_cost = @original_min_cost
+ end
+
+ test "automatically include ActiveModel::Validations when validations are enabled" do
+ assert_respond_to @user, :valid?
end
- test "create and updating without validations" do
- assert @visitor.valid?(:create), 'visitor should be valid'
- assert @visitor.valid?(:update), 'visitor should be valid'
+ test "don't include ActiveModel::Validations when validations are disabled" do
+ assert_not_respond_to @visitor, :valid?
+ end
+
+ test "create a new user with validations and valid password/confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'password'
+
+ assert @user.valid?(:create), 'user should be valid'
+
+ @user.password = 'a' * 72
+ @user.password_confirmation = 'a' * 72
- @visitor.password = '123'
- @visitor.password_confirmation = '456'
+ assert @user.valid?(:create), 'user should be valid'
+ end
- assert @visitor.valid?(:create), 'visitor should be valid'
- assert @visitor.valid?(:update), 'visitor should be valid'
+ test "create a new user with validation and a spaces only password" do
+ @user.password = ' ' * 72
+ assert @user.valid?(:create), 'user should be valid'
end
test "create a new user with validation and a blank password" do
@@ -43,6 +59,14 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_equal ["can't be blank"], @user.errors[:password]
end
+ test 'create a new user with validation and password length greater than 72' do
+ @user.password = 'a' * 73
+ @user.password_confirmation = 'a' * 73
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password]
+ end
+
test "create a new user with validation and a blank password confirmation" do
@user.password = 'password'
@user.password_confirmation = ''
@@ -65,15 +89,19 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end
- 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]
+ test "update an existing user with validation and no change in password" do
+ assert @existing_user.valid?(:update), 'user should be valid'
end
- test "update an existing user with validation and no change in password" do
+ test "update an existing user with validations and valid password/confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'password'
+
+ assert @existing_user.valid?(:update), 'user should be valid'
+
+ @existing_user.password = 'a' * 72
+ @existing_user.password_confirmation = 'a' * 72
+
assert @existing_user.valid?(:update), 'user should be valid'
end
@@ -82,6 +110,11 @@ class SecurePasswordTest < ActiveModel::TestCase
assert @existing_user.valid?(:update), 'user should be valid'
end
+ test "updating an existing user with validation and a spaces only password" do
+ @user.password = ' ' * 72
+ assert @user.valid?(:update), 'user should be valid'
+ end
+
test "updating an existing user with validation and a blank password and password_confirmation" do
@existing_user.password = ''
@existing_user.password_confirmation = ''
@@ -95,6 +128,14 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_equal ["can't be blank"], @existing_user.errors[:password]
end
+ test 'updating an existing user with validation and password length greater than 72' do
+ @existing_user.password = 'a' * 73
+ @existing_user.password_confirmation = 'a' * 73
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["is too long (maximum is 72 characters)"], @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 = ''
@@ -117,14 +158,6 @@ class SecurePasswordTest < ActiveModel::TestCase
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'
@@ -147,7 +180,7 @@ class SecurePasswordTest < ActiveModel::TestCase
test "setting a nil password should clear an existing password" do
@existing_user.password = nil
assert_equal nil, @existing_user.password_digest
- end
+ end
test "authenticate" do
@user.password = "secret"
@@ -164,11 +197,16 @@ class SecurePasswordTest < ActiveModel::TestCase
end
test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
- ActiveModel::SecurePassword.min_cost = false
- BCrypt::Engine.cost = 5
-
- @user.password = "secret"
- assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ 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
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index bc185c737f..734656b749 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -1,6 +1,5 @@
require 'cases/helper'
require 'models/contact'
-require 'models/automobile'
require 'active_support/core_ext/object/instance_variables'
class Contact
@@ -30,11 +29,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 +41,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,13 +145,18 @@ 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
diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb
index 11ee17bb27..5db14c8157 100644
--- a/activemodel/test/cases/serializers/xml_serialization_test.rb
+++ b/activemodel/test/cases/serializers/xml_serialization_test.rb
@@ -57,48 +57,48 @@ class XmlSerializationTest < ActiveModel::TestCase
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
@@ -106,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
@@ -162,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 795ce16d28..ebfe1cf4e4 100644
--- a/activemodel/test/cases/validations/absence_validation_test.rb
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -11,7 +11,7 @@ class AbsenceValidationTest < ActiveModel::TestCase
CustomReader.clear_validators!
end
- def test_validate_absences
+ def test_validates_absence_of
Topic.validates_absence_of(:title, :content)
t = Topic.new
t.title = "foo"
@@ -23,11 +23,12 @@ class AbsenceValidationTest < ActiveModel::TestCase
t.content = "something"
assert t.invalid?
assert_equal ["must be blank"], t.errors[:content]
+ assert_equal [], t.errors[:title]
t.content = ""
assert t.valid?
end
- def test_accepts_array_arguments
+ def test_validates_absence_of_with_array_arguments
Topic.validates_absence_of %w(title content)
t = Topic.new
t.title = "foo"
@@ -37,7 +38,7 @@ class AbsenceValidationTest < ActiveModel::TestCase
assert_equal ["must be blank"], t.errors[:content]
end
- def test_validates_acceptance_of_with_custom_error_using_quotes
+ def test_validates_absence_of_with_custom_error_using_quotes
Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes"
p = Person.new
p.karma = "good"
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index 4957ba5d0a..65a2a1eb49 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -53,22 +53,25 @@ class ConfirmationValidationTest < ActiveModel::TestCase
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/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
index 93600c587a..3eeb80a48b 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -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 d10010537e..96084a32ba 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -19,6 +19,7 @@ class I18nValidationTest < ActiveModel::TestCase
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/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index f77cf47fb7..3834d327ea 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -50,6 +50,21 @@ class NumericalityValidationTest < ActiveModel::TestCase
valid!(NIL + INTEGERS)
end
+ def test_validates_numericality_of_with_integer_only_and_symbol_as_value
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_proc_as_value
+ Topic.send(:define_method, :allow_only_integers?, lambda { false })
+ Topic.validates_numericality_of :approved, only_integer: Proc.new {|topic| topic.allow_only_integers? }
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
def test_validates_numericality_with_greater_than
Topic.validates_numericality_of :approved, greater_than: 10
@@ -119,6 +134,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
invalid!([3, 4])
valid!([5, 6])
+ ensure
Topic.send(:remove_method, :min_approved)
end
@@ -128,6 +144,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
invalid!([6])
valid!([4, 5])
+ ensure
Topic.send(:remove_method, :max_approved)
end
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index bee8ece992..ba0aacc2a5 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -4,7 +4,6 @@ require 'cases/helper'
require 'models/topic'
require 'models/reply'
require 'models/custom_reader'
-require 'models/automobile'
require 'active_support/json'
require 'active_support/xml_mini'
@@ -139,6 +138,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
@@ -165,6 +166,13 @@ class ValidationsTest < ActiveModel::TestCase
end
end
+ def test_invalid_options_to_validate
+ assert_raises(ArgumentError) do
+ # A common mistake -- we meant to call 'validates'
+ Topic.validate :title, presence: true
+ end
+ end
+
def test_errors_conversions
Topic.validates_presence_of %w(title content)
t = Topic.new
@@ -281,15 +289,30 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_validations_on_the_instance_level
- auto = Automobile.new
+ Topic.validates :title, :author_name, presence: true
+ Topic.validates :content, length: { minimum: 10 }
- assert auto.invalid?
- assert_equal 2, auto.errors.size
+ topic = Topic.new
+ assert topic.invalid?
+ assert_equal 3, topic.errors.size
- auto.make = 'Toyota'
- auto.model = 'Corolla'
+ topic.title = 'Some Title'
+ topic.author_name = 'Some Author'
+ topic.content = 'Some Content Whose Length is more than 10.'
+ assert topic.valid?
+ end
- assert auto.valid?
+ def test_validate
+ Topic.validate do
+ validates_presence_of :title, :author_name
+ validates_length_of :content, minimum: 10
+ end
+
+ topic = Topic.new
+ assert_empty topic.errors
+
+ topic.validate
+ assert_not_empty topic.errors
end
def test_strict_validation_in_validates
@@ -366,25 +389,4 @@ class ValidationsTest < ActiveModel::TestCase
assert topic.invalid?
assert duped.valid?
end
-
- # validator test:
- def test_setup_is_deprecated_but_still_receives_klass # TODO: remove me in 4.2.
- validator_class = Class.new(ActiveModel::Validator) do
- def setup(klass)
- @old_klass = klass
- end
-
- def validate(*)
- @old_klass == Topic or raise "#setup didn't work"
- end
- end
-
- assert_deprecated do
- Topic.validates_with validator_class
- end
-
- t = Topic.new
- t.valid?
- end
-
end
diff --git a/activemodel/test/models/automobile.rb b/activemodel/test/models/automobile.rb
deleted file mode 100644
index ece644c40c..0000000000
--- a/activemodel/test/models/automobile.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class Automobile
- include ActiveModel::Validations
-
- validate :validations
-
- attr_accessor :make, :model
-
- def validations
- validates_presence_of :make
- validates_length_of :model, within: 2..10
- end
-end
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index cbe259b1ad..1ec6001c48 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -1,6 +1,5 @@
class User
extend ActiveModel::Callbacks
- include ActiveModel::Validations
include ActiveModel::SecurePassword
define_model_callbacks :create
diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb
index 4d7f4be097..22ad1a3c3d 100644
--- a/activemodel/test/models/visitor.rb
+++ b/activemodel/test/models/visitor.rb
@@ -1,6 +1,5 @@
class Visitor
extend ActiveModel::Callbacks
- include ActiveModel::Validations
include ActiveModel::SecurePassword
define_model_callbacks :create
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 460eeb5dfd..7d7870fe13 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -4,1806 +4,1089 @@
*Sergey Potapov*
-* Properly detect if a connection is still active before using it
- in multi-threaded environments.
-
- Fixes #12867.
-
- *Kevin Casey*, *Matthew Draper*, *William (B.J.) Snow Orvis*
-
-* When inverting add_index use the index name if present instead of
- the columns.
-
- If there are two indices with matching columns and one of them is
- explicitly named then reverting the migration adding the named one
- would instead drop the unnamed one.
-
- The inversion of add_index will now drop the index by its name if
- it is present.
-
- *Hubert Dąbrowski*
-
-* Add flag to disable schema dump after migration.
-
- Add a config parameter on Active Record named `dump_schema_after_migration`
- which is true by default. Now schema dump does not happen at the
- end of migration rake task if `dump_schema_after_migration` is false.
-
- *Emil Soman*
-
-* `find_in_batches`, `find_each`, `Result#each` and `Enumerable#index_by` now
- return an `Enumerator` that can calculate its size.
-
- See also #13938.
-
- *Marc-André Lafortune*
-
-* Make sure transaction state gets reset after a commit operation on the record.
-
- If a new transaction was open inside a callback, the record was loosing track
- of the transaction level state, and it was leaking that state.
-
- Fixes #12566.
-
- *arthurnn*
-
-* Pass `has_and_belongs_to_many` `:autosave` option to
- the underlying `has_many :through` association.
-
- Fixes #13923.
-
- *Yves Senn*
-
-* PostgreSQL implementation of `SchemaStatements#index_name_exists?`.
-
- The database agnostic implementation does not detect with indexes that are
- not supported by the ActiveRecord schema dumper. For example, expressions
- indexes would not be detected.
-
- Fixes #11018.
-
- *Jonathan Baudanza*
-
-* Parsing PostgreSQL arrays with empty strings now works correctly.
-
- Previously, if you tried to parse `{"1","","2","","3"}` the result
- would be `["1","2","3"]`, removing the empty strings from the array,
- which would be incorrect. Now it will correctly produce `["1","","2","","3"]`
- as the result of parsing the above PostgreSQL array.
-
- Fixes #13907.
-
- *Maurício Linhares*
-
-* Associations now raise `ArgumentError` on name conflicts.
-
- Dangerous association names conflicts include instance or class methods already
- defined by `ActiveRecord::Base`.
+* `index_exists?` with `:name` option does verify specified columns.
Example:
- class Car < ActiveRecord::Base
- has_many :errors
- end
- # Will raise ArgumentError.
-
- Fixes #13217.
-
- *Lauro Caetano*
-
-* Fix regressions on `select_*` methods.
- When `select_*` methods receive a `Relation` object, they should be able to
- get the arel/binds from it.
- Also fix regressions on `select_rows` that was ignoring the binds.
-
- Fixes #7538, #12017, #13731, #12056.
-
- *arthurnn*
-
-* Active Record objects can now be correctly dumped, loaded and dumped again
- without issues.
-
- Previously, if you did `YAML.dump`, `YAML.load` and then `YAML.dump` again
- in an Active Record model that used serialization it would fail at the last
- dump due to the fields not being correctly serialized before being dumped
- to YAML. Now it is possible to dump and load the same object as many times
- as needed without any issues.
-
- Fixes #13861.
-
- *Maurício Linhares*
-
-* `find_in_batches` now returns an `Enumerator` when called without a block, so that it
- can be chained with other `Enumerable` methods.
-
- *Marc-André Lafortune*
-
-* `enum` now raises on "dangerous" name conflicts.
-
- Dangerous name conflicts includes instance or class method conflicts
- with methods defined within `ActiveRecord::Base` but not its ancestors,
- as well as conflicts with methods generated by other enums on the same
- class.
-
- Fixes #13389.
-
- *Godfrey Chan*
-
-* `scope` now raises on "dangerous" name conflicts.
+ add_index :articles, :title, name: "idx_title"
- Similar to dangerous attribute methods, a scope name conflict is
- dangerous if it conflicts with an existing class method defined within
- `ActiveRecord::Base` but not its ancestors.
-
- See also #13389.
-
- *Godfrey Chan*, *Philippe Creux*
-
-* Correctly send an user provided statement to a `lock!()` call.
-
- person.lock! 'FOR SHARE NOWAIT'
- # Before: SELECT * ... LIMIT 1 FOR UPDATE
- # After: SELECT * ... LIMIT 1 FOR SHARE NOWAIT
-
- Fixes #13788.
-
- *Maurício Linhares*
-
-* Handle aliased attributes `select()`, `order()` and `reorder()`.
-
- *Tsutomu Kuroda*
-
-* Reset the collection association when calling `reset` on it.
-
- Before:
-
- post.comments.loaded? # => true
- post.comments.reset
- post.comments.loaded? # => true
-
- After:
-
- post.comments.loaded? # => true
- post.comments.reset
- post.comments.loaded? # => false
-
- Fixes #13777.
-
- *Kelsey Schlarman*
-
-* Make enum fields work as expected with the `ActiveModel::Dirty` API.
-
- Before this change, using the dirty API would have surprising results:
+ # Before:
+ index_exists? :articles, :title, name: "idx_title" # => `true`
+ index_exists? :articles, :body, name: "idx_title" # => `true`
- conversation = Conversation.new
- conversation.status = :active
- conversation.status = :archived
- conversation.status_was # => 0
+ # After:
+ index_exists? :articles, :title, name: "idx_title" # => `true`
+ index_exists? :articles, :body, name: "idx_title" # => `false`
- After this change, the same code would result in:
+ *Yves Senn*, *Matthew Draper*
- conversation = Conversation.new
- conversation.status = :active
- conversation.status = :archived
- conversation.status_was # => "active"
+* When calling `update_columns` on a record that is not persisted, the error
+ message now reflects whether that object is a new record or has been
+ destroyed.
- *Rafael Mendonça França*
+ *Lachlan Sylvester*
-* `has_one` and `belongs_to` accessors don't add ORDER BY to the queries
- anymore.
+* Define `id_was` to get the previous value of the primary key.
- Since Rails 4.0, we add an ORDER BY in the `first` method to ensure
- consistent results among different database engines. But for singular
- associations this behavior is not needed since we will have one record to
- return. As this ORDER BY option can lead some performance issues we are
- removing it for singular associations accessors.
+ Currently when we call id_was and we have a custom primary key name
+ Active Record will return the current value of the primary key. This
+ make impossible to correctly do an update operation if you change the
+ id.
- Fixes #12623.
+ Fixes #16413.
*Rafael Mendonça França*
-* Prepend table name for column names passed to `Relation#select`.
-
- Example:
-
- Post.select(:id)
- # Before: => SELECT id FROM "posts"
- # After: => SELECT "posts"."id" FROM "posts"
+* Deprecate `DatabaseTasks.load_schema` to act on the current connection.
+ Use `.load_schema_current` instead. In the future `load_schema` will
+ require the `configuration` to act on as an argument.
*Yves Senn*
-* Fail early with "Primary key not included in the custom select clause"
- in `find_in_batches`.
+* Fixed automatic maintaining test schema to properly handle sql structure
+ schema format.
- Before this patch, the exception was raised after the first batch was
- yielded to the block. This means that you only get it, when you hit the
- `batch_size` treshold. This could shadow the issue in development.
+ Fixes #15394.
- *Alexander Balashov*
+ *Wojciech Wnętrzak*
-* Ensure `second` through `fifth` methods act like the `first` finder.
+* Fix type casting to Decimal from Float with large precision.
- The famous ordinal Array instance methods defined in ActiveSupport
- (`first`, `second`, `third`, `fourth`, and `fifth`) are now available as
- full-fledged finders in ActiveRecord. The biggest benefit of this is ordering
- of the records returned now defaults to the table's primary key in ascending order.
+ *Tomohiro Hashidate*
- Fixes #13743.
+* Deprecate `Reflection#source_macro`
- Example:
+ `Reflection#source_macro` is no longer needed in Active Record
+ source so it has been deprecated. Code that used `source_macro`
+ was removed in #16353.
- User.all.second
+ *Eileen M. Uchtitelle*, *Aaron Patterson*
- # Before
- # => 'SELECT "users".* FROM "users"'
+* No verbose backtrace by db:drop when database does not exist.
- # After
- # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1'
+ Fixes #16295.
- User.offset(3).second
+ *Kenn Ejima*
- # Before
- # => 'SELECT "users".* FROM "users" LIMIT -1 OFFSET 3' # sqlite3 gem
- # => 'SELECT "users".* FROM "users" OFFSET 3' # pg gem
- # => 'SELECT `users`.* FROM `users` LIMIT 18446744073709551615 OFFSET 3' # mysql2 gem
-
- # After
- # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 4'
-
- *Jason Meller*
-
-* ActiveRecord states are now correctly restored after a rollback for
- models that did not define any transactional callbacks (i.e.
- `after_commit`, `after_rollback` or `after_create`).
-
- Fixes #13744.
-
- *Godfrey Chan*
-
-* Make `touch` fire the `after_commit` and `after_rollback` callbacks.
-
- *Harry Brundage*
-
-* Enable partial indexes for `sqlite >= 3.8.0`.
-
- See http://www.sqlite.org/partialindex.html
-
- *Cody Cutrer*
-
-* Don't try to get the subclass if the inheritance column doesn't exist
-
- The `subclass_from_attrs` method is called even if the column specified by
- the `inheritance_column` setting doesn't exist. This prevents setting associations
- via the attributes hash if the association name clashes with the value of the setting,
- typically `:type`. This worked previously in Rails 3.2.
-
- *Ujjwal Thaakar*
-
-* Enum mappings are now exposed via class methods instead of constants.
+* Add support for Postgresql JSONB.
Example:
- class Conversation < ActiveRecord::Base
- enum status: [ :active, :archived ]
+ create_table :posts do |t|
+ t.jsonb :meta_data
end
- Before:
-
- Conversation::STATUS # => { "active" => 0, "archived" => 1 }
-
- After:
-
- Conversation.statuses # => { "active" => 0, "archived" => 1 }
-
- *Godfrey Chan*
-
-* Set `NameError#name` when STI-class-lookup fails.
-
- *Chulki Lee*
-
-* Fix bug in `becomes!` when changing from the base model to a STI sub-class.
+ *Philippe Creux*, *Chris Teague*
- Fixes #13272.
-
- *the-web-dev*, *Yves Senn*
-
-* Currently Active Record can be configured via the environment variable
- `DATABASE_URL` or by manually injecting a hash of values which is what Rails does,
- reading in `database.yml` and setting Active Record appropriately. Active Record
- expects to be able to use `DATABASE_URL` without the use of Rails, and we cannot
- rip out this functionality without deprecating. This presents a problem though
- when both config is set, and a `DATABASE_URL` is present. Currently the
- `DATABASE_URL` should "win" and none of the values in `database.yml` are
- used. This is somewhat unexpected, if one were to set values such as
- `pool` in the `production:` group of `database.yml` they are ignored.
-
- There are many ways that Active Record initiates a connection today:
-
- - Stand Alone (without rails)
- - `rake db:<tasks>`
- - `ActiveRecord.establish_connection`
-
- - With Rails
- - `rake db:<tasks>`
- - `rails <server> | <console>`
- - `rails dbconsole`
-
- Now all of these behave exactly the same way. The best way to do
- this is to put all of this logic in one place so it is guaranteed to be used.
-
- Here is the matrix of how this behavior works:
-
- ```
- No database.yml
- No DATABASE_URL
- => Error
- ```
-
- ```
- database.yml present
- No DATABASE_URL
- => Use database.yml configuration
- ```
-
- ```
- No database.yml
- DATABASE_URL present
- => use DATABASE_URL configuration
- ```
-
- ```
- database.yml present
- DATABASE_URL present
- => Merged into `url` sub key. If both specify `url` sub key, the `database.yml` `url`
- sub key "wins". If other paramaters `adapter` or `database` are specified in YAML,
- they are discarded as the `url` sub key "wins".
- ```
-
- Current implementation uses `ActiveRecord::Base.configurations` to resolve and merge
- all connection information before returning. This is achieved through a utility
- class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`.
-
- To understand the exact behavior of this class, it is best to review the
- behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`.
-
- *Richard Schneeman*
-
-* Make `change_column_null` revertable. Fixes #13576.
-
- *Yves Senn*, *Nishant Modak*, *Prathamesh Sonpatki*
-
-* Don't create/drop the test database if RAILS_ENV is specified explicitly.
-
- Previously, when the environment was development, we would always
- create or drop both the test and development databases.
-
- Now, if RAILS_ENV is explicitly defined as development, we don't create
- the test database.
-
- *Damien Mathieu*
-
-* Initialize version on Migration objects so that it can be used in a migration,
- and it will be included in the announce message.
-
- *Dylan Thacker-Smith*
-
-* `change_table` now uses the current adapter's `update_table_definition`
- method to retrieve a specific table definition.
- This ensures that `change_table` and `create_table` will use
- similar objects.
-
- Fixes #13577, #13503.
-
- *Nishant Modak*, *Prathamesh Sonpatki*, *Rafael Mendonça França*
-
-* Fixed ActiveRecord::Store nil conversion TypeError when using YAML coder.
- In case the YAML passed as paramter is nil, uses an empty string.
-
- Fixes #13570.
-
- *Thales Oliveira*
-
-* Deprecate unused `ActiveRecord::Base.symbolized_base_class`
- and `ActiveRecord::Base.symbolized_sti_name` without replacement.
+* `db:purge` with MySQL respects `Rails.env`.
*Yves Senn*
-* Since the `test_help.rb` file in Railties now automatically maintains
- your test schema, the `rake db:test:*` tasks are deprecated. This
- doesn't stop you manually running other tasks on your test database
- if needed:
-
- rake db:schema:load RAILS_ENV=test
-
- *Jon Leighton*
-
-* Fix presence validator for association when the associated record responds to `to_a`.
-
- *gmarik*
-
-* Fixed regression on preload/includes with multiple arguments failing in certain conditions,
- raising a NoMethodError internally by calling `reflect_on_association` for `NilClass:Class`.
-
- Fixes #13437.
-
- *Vipul A M*, *khustochka*
-
-* Add the ability to nullify the `enum` column.
-
- Example:
+* `change_column_default :table, :column, nil` with PostgreSQL will issue a
+ `DROP DEFAULT` instead of a `DEFAULT NULL` query.
- class Conversation < ActiveRecord::Base
- enum gender: [:female, :male]
- end
+ Fixes #16261.
- Conversation::GENDER # => { female: 0, male: 1 }
+ *Matthew Draper*, *Yves Senn*
- # conversation.update! gender: 0
- conversation.female!
- conversation.female? # => true
- conversation.gender # => "female"
-
- # conversation.update! gender: nil
- conversation.gender = nil
- conversation.gender.nil? # => true
- conversation.gender # => nil
-
- *Amr Tamimi*
-
-* Connection specification now accepts a "url" key. The value of this
- key is expected to contain a database URL. The database URL will be
- expanded into a hash and merged.
-
- *Richard Schneeman*
-
-* An `ArgumentError` is now raised on a call to `Relation#where.not(nil)`.
+* Allow to specify a type for the foreign key column in `references`
+ and `add_reference`.
Example:
- User.where.not(nil)
-
- # Before
- # => 'SELECT `users`.* FROM `users` WHERE (NOT (NULL))'
-
- # After
- # => ArgumentError, 'Invalid argument for .where.not(), got nil.'
-
- *Kuldeep Aggarwal*
-
-* Deprecated use of string argument as a configuration lookup in
- `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given.
-
- *José Valim*
-
-* Fixed `update_column`, `update_columns`, and `update_all` to correctly serialize
- values for `array`, `hstore` and `json` column types in PostgreSQL.
-
- Fixes #12261.
-
- *Tadas Tamosauskas*, *Carlos Antonio da Silva*
-
-* Do not consider PostgreSQL array columns as number or text columns.
-
- The code uses these checks in several places to know what to do with a
- particular column, for instance AR attribute query methods has a branch
- like this:
-
- if column.number?
- !value.zero?
+ change_table :vehicle do |t|
+ t.references :station, type: :uuid
end
- This should never be true for array columns, since it would be the same
- as running [].zero?, which results in a NoMethodError exception.
-
- Fixing this by ensuring that array columns in PostgreSQL never return
- true for number?/text? checks.
-
- *Carlos Antonio da Silva*
-
-* When connecting to a non-existant database, the error:
- `ActiveRecord::NoDatabaseError` will now be raised. When being used with Rails
- the error message will include information on how to create a database:
- `rake db:create`. Supported adapters: postgresql, mysql, mysql2, sqlite3
-
- *Richard Schneeman*
-
-* Do not raise `'cannot touch on a new record object'` exception on destroying
- already destroyed `belongs_to` association with `touch: true` option.
-
- Fixes #13445.
-
- Example:
-
- # Given Comment has belongs_to :post, touch: true
- comment.post.destroy
- comment.destroy # no longer raises an error
-
- *Paul Nikitochkin*
-
-* Fix a bug when assigning an array containing string numbers to a
- PostgreSQL integer array column.
-
- Fixes #13444.
-
- Example:
-
- # Given Book#ratings is of type :integer, array: true
- Book.new(ratings: [1, 2]) # worked before
- Book.new(ratings: ['1', '2']) # now works as well
-
- *Damien Mathieu*
-
-* Improve the default select when `from` is used.
-
- Previously, if you did something like Topic.from(:temp_topics), it
- would generate SQL like:
-
- SELECT topics.* FROM temp_topics;
-
- Which is will cause an error since there's not a topics table to select
- from.
+ *Andrey Novikov*, *Łukasz Sarnacki*
- Now the default if you use from is just `*`:
+* `create_join_table` removes a common prefix when generating the join table.
+ This matches the existing behavior of HABTM associations.
- SELECT * FROM temp_topics;
+ Fixes #13683.
- *Cody Cutrer*
+ *Stefan Kanev*
-* Fix `PostgreSQL` insert to properly extract table name from multiline string SQL.
+* Dont swallow errors on compute_type when having a bad alias_method on
+ a class.
- Previously, executing an insert SQL in `PostgreSQL` with a command like this:
-
- insert into articles(
- number)
- values(
- 5152
- )
+ *arthurnn*
- would not work because the adapter was unable to extract the correct `articles`
- table name.
+* PostgreSQL invalid `uuid` are convert to nil.
- *Kuldeep Aggarwal*
+ *Abdelkader Boudih*
-* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert
- to an `Array` by calling `#to_a` before using these methods.
+* Restore 4.0 behavior for using serialize attributes with `JSON` as coder.
- It intends to prevent odd bugs and confusion in code that call mutator
- methods directly on the `Relation`.
+ With 4.1.x, `serialize` started returning a string when `JSON` was passed as
+ the second attribute. It will now return a hash as per previous versions.
Example:
- # 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!
-
- *Lauro Caetano*
-
-* Better support for `where()` conditions that use a `belongs_to`
- association name.
-
- Using the name of an association in `where` previously worked only
- if the value was a single `ActiveRecord::Base` object. e.g.
-
- Post.where(author: Author.first)
-
- Any other values, including `nil`, would cause invalid SQL to be
- generated. This change supports arguments in the `where` query
- conditions where the key is a `belongs_to` association name and the
- value is `nil`, an `Array` of `ActiveRecord::Base` objects, or an
- `ActiveRecord::Relation` object.
-
class Post < ActiveRecord::Base
- belongs_to :author
+ serialize :comment, JSON
end
- `nil` value finds records where the association is not set:
-
- Post.where(author: nil)
- # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IS NULL
-
- `Array` values find records where the association foreign key
- matches the ids of the passed ActiveRecord models, resulting
- in the same query as `Post.where(author_id: [1,2])`:
-
- authors_array = [Author.find(1), Author.find(2)]
- Post.where(author: authors_array)
- # SELECT "posts".* FROM "posts" WHERE "posts"."author_id" IN (1, 2)
-
- `ActiveRecord::Relation` values find records using the same
- query as `Post.where(author_id: Author.where(last_name: "Emde"))`
-
- Post.where(author: Author.where(last_name: "Emde"))
- # SELECT "posts".* FROM "posts"
- # WHERE "posts"."author_id" IN (
- # SELECT "authors"."id" FROM "authors"
- # WHERE "authors"."last_name" = 'Emde')
-
- Polymorphic `belongs_to` associations will continue to be handled
- appropriately, with the polymorphic `association_type` field added
- to the query to match the base class of the value. This feature
- previously only worked when the value was a single `ActveRecord::Base`.
-
- class Post < ActiveRecord::Base
- belongs_to :author, polymorphic: true
+ class Comment
+ include ActiveModel::Model
+ attr_accessor :category, :text
end
- Post.where(author: Author.where(last_name: "Emde"))
- # Generates a query similar to:
- Post.where(author_id: Author.where(last_name: "Emde"), author_type: "Author")
-
- *Martin Emde*
-
-* Respect temporary option when dropping tables with MySQL.
+ post = Post.create!
+ post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.")
+ post.save!
- Normal DROP TABLE also works, but commits the transaction.
+ # 4.0
+ post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."}
- drop_table :temporary_table, temporary: true
+ # 4.1 before
+ post.comment # => "#<Comment:0x007f80ab48ff98>"
- *Cody Cutrer*
+ # 4.1 after
+ post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."}
-* Add option to create tables from a query.
+ When using `JSON` as the coder in `serialize`, Active Record will use the
+ new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to
+ `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped
+ correctly using the `#as_json` hook.
- create_table(:long_query, temporary: true,
- as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+ To keep the previous behaviour, supply a custom coder instead
+ ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)).
- Generates:
+ Fixes #15594.
- CREATE TEMPORARY TABLE long_query AS
- SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
+ *Jenn Cooper*
- *Cody Cutrer*
+* Do not use `RENAME INDEX` syntax for MariaDB 10.0.
-* `db:test:clone` and `db:test:prepare` must load Rails environment.
+ Fixes #15931.
- `db:test:clone` and `db:test:prepare` use `ActiveRecord::Base`. configurations,
- so we need to load the Rails environment, otherwise the config wont be in place.
+ *Jeff Browning*
- *arthurnn*
+* Calling `#empty?` on a `has_many` association would use the value from the
+ counter cache if one exists.
-* Use the right column to type cast grouped calculations with custom expressions.
+ *David Verhasselt*
- Fixes #13230.
+* Fix the schema dump generated for tables without constraints and with
+ primary key with default value of custom PostgreSQL function result.
- Example:
+ Fixes #16111.
- # Before
- Account.group(:firm_name).sum('0.01 * credit_limit')
- # => { '37signals' => '0.5' }
+ *Andrey Novikov*
- # After
- Account.group(:firm_name).sum('0.01 * credit_limit')
- # => { '37signals' => 0.5 }
+* Fix the SQL generated when a `delete_all` is run on an association to not
+ produce an `IN` statements.
- *Paul Nikitochkin*
-
-* Polymorphic `belongs_to` associations with the `touch: true` option set update the timestamps of
- the old and new owner correctly when moved between owners of different types.
-
- Example:
-
- class Rating < ActiveRecord::Base
- belongs_to :rateable, polymorphic: true, touch: true
- end
-
- rating = Rating.create rateable: Song.find(1)
- rating.update_attributes rateable: Book.find(2) # => timestamps of Song(1) and Book(2) are updated
-
- *Severin Schoepke*
-
-* Improve formatting of migration exception messages: make them easier to read
- with line breaks before/after, and improve the error for pending migrations.
-
- *John Bachir*
-
-* Fix `last` with `offset` to return the proper record instead of always the last one.
+ Before:
- Example:
+ UPDATE "categorizations" SET "category_id" = NULL WHERE
+ "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2)
- Model.offset(4).last
- # => returns the 4th record from the end.
+ After:
- Fixes #7441.
+ UPDATE "categorizations" SET "category_id" = NULL WHERE
+ "categorizations"."category_id" = 1
- *kostya*, *Lauro Caetano*
+ *Eileen M. Uchitelle, Aaron Patterson*
-* `type_to_sql` returns a `String` for unmapped columns. This fixes an error
- when using unmapped PostgreSQL array types.
+* Avoid type casting boolean and ActiveSupport::Duration values to numeric
+ values for string columns. Otherwise, in some database, the string column
+ values will be coerced to a numeric allowing false or 0.seconds match any
+ string starting with a non-digit.
Example:
- change_colum :table, :column, :bigint, array: true
+ App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0'
- Fixes #13146.
-
- *Jens Fahnenbruck*, *Yves Senn*
+ *Dylan Thacker-Smith*
-* Fix `QueryCache` to work with nested blocks, so that it will only clear the existing cache
- after leaving the outer block instead of clearing it right after the inner block is finished.
+* Add a `:required` option to singular associations, providing a nicer
+ API for presence validations on associations.
- *Vipul A M*
+ *Sean Griffin*
-* 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 the
- class object returned by `ActiveRecord::FixtureSet.context_class`.
+* Fixed error in `reset_counters` when associations have `select` scope.
+ (Call to `count` generates invalid SQL.)
- *Victor Costan*
+ *Cade Truitt*
-* Previously, the `has_one` macro incorrectly accepted the `counter_cache`
- option, but never actually supported it. Now it will raise an `ArgumentError`
- when using `has_one` with `counter_cache`.
+* After a successful `reload`, `new_record?` is always false.
- *Godfrey Chan*
+ Fixes #12101.
-* Implement `rename_index` natively for MySQL >= 5.7.
+ *Matthew Draper*
- *Cody Cutrer*
+* PostgreSQL renaming table doesn't attempt to rename non existent sequences.
-* Fix bug when validating the uniqueness of an aliased attribute.
+ *Abdelkader Boudih*
- Fixes #12402.
+* Move 'dependent: :destroy' handling for 'belongs_to'
+ from 'before_destroy' to 'after_destroy' callback chain
- *Lauro Caetano*
+ Fixes #12380.
-* Update counter cache on a `has_many` relationship regardless of default scope.
+ *Ivan Antropov*
- Fixes #12952.
+* Detect in-place modifications on String attributes.
- *Uku Taht*
+ Before this change user have to mark the attribute as changed to it be persisted
+ in the database. Now it is not required anymore.
-* `rename_index` adds the new index before removing the old one. This allows to
- rename indexes on columns with a foreign key and prevents the following error:
+ Before:
- Cannot drop index 'index_engines_on_car_id': needed in a foreign key constraint
+ user = User.first
+ user.name << ' Griffin'
+ user.name_will_change!
+ user.save
+ user.reload.name # => "Sean Griffin"
- *Cody Cutrer*, *Yves Senn*
+ After:
-* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child
- marked with `dependent: destroy` fails to be destroyed.
+ user = User.first
+ user.name << ' Griffin'
+ user.save
+ user.reload.name # => "Sean Griffin"
- Fixex #12812.
+ *Sean Griffin*
- *Brian Thomas Storti*
+* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record
+ is invalid.
-* Fix validation on uniqueness of empty association.
+ *Bogdan Gusiev*, *Marc Schütz*
- *Evgeny Li*
+* Support for adding and removing foreign keys. Foreign keys are now
+ a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter
+ and PostgreSQLAdapter.
-* Make `ActiveRecord::Relation#unscope` affect relations it is merged in to.
+ Many thanks to *Matthew Higgins* for laying the foundation with his work on
+ [foreigner](https://github.com/matthuhiggins/foreigner).
- *Jon Leighton*
+ Example:
-* Use strings to represent non-string `order_values`.
+ # within your migrations:
+ add_foreign_key :articles, :authors
+ remove_foreign_key :articles, :authors
*Yves Senn*
-* Checks to see if the record contains the foreign key to set the inverse automatically.
+* Fix subtle bugs regarding attribute assignment on models with no primary
+ key. `'id'` will no longer be part of the attributes hash.
- *Edo Balvers*
+ *Sean Griffin*
-* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from a model's attribute or method.
+* Deprecate automatic counter caches on `has_many :through`. The behavior was
+ broken and inconsistent.
- Example:
+ *Sean Griffin*
- class User < ActiveRecord::Base
- to_param :name
- end
+* `preload` preserves readonly flag for associations.
- user = User.find_by(name: 'Fancy Pants')
- user.id # => 123
- user.to_param # => "123-fancy-pants"
+ See #15853.
- *Javan Makhmali*
+ *Yves Senn*
-* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on models.
+* Assume numeric types have changed if they were assigned to a value that
+ would fail numericality validation, regardless of the old value. Previously
+ this would only occur if the old value was 0.
Example:
- Post.no_touching do
- Post.first.touch
- end
+ model = Model.create!(number: 5)
+ model.number = '5wibble'
+ model.number_changed? # => true
- *Sam Stephenson*, *Damien Mathieu*
+ Fixes #14731.
-* Prevent the counter cache from being decremented twice when destroying
- a record on a `has_many :through` association.
+ *Sean Griffin*
- Fixes #11079.
+* `reload` no longer merges with the existing attributes.
+ The attribute hash is fully replaced. The record is put into the same state
+ as it would be with `Model.find(model.id)`.
- *Dmitry Dedov*
+ *Sean Griffin*
-* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`.
- `type_cast` will return `1` for `true` and `0` for `false`.
+* The object returned from `select_all` must respond to `column_types`.
+ If this is not the case a `NoMethodError` is raised.
- Fixes #11119.
+ *Sean Griffin*
- *Adam Williams*, *Yves Senn*
+* `has_many :through` associations will no longer save the through record
+ twice when added in an `after_create` callback defined before the
+ associations.
-* Fix bug where `has_one` association record update result in crash, when replaced with itself.
+ Fixes #3798.
- Fixes #12834.
+ *Sean Griffin*
- *Denis Redozubov*, *Sergio Cambra*
+* Detect in-place modifications of PG array types
-* Log bind variables after they are type casted. This makes it more
- transparent what values are actually sent to the database.
+ *Sean Griffin*
- irb(main):002:0> Event.find("im-no-integer")
- # Before: ... WHERE "events"."id" = $1 LIMIT 1 [["id", "im-no-integer"]]
- # After: ... WHERE "events"."id" = $1 LIMIT 1 [["id", 0]]
+* Add `bin/rake db:purge` task to empty the current database.
*Yves Senn*
-* Fix uninitialized constant `TransactionState` error when `Marshall.load` is used on an Active Record result.
+* Deprecate `serialized_attributes` without replacement.
- Fixes #12790.
+ *Sean Griffin*
- *Jason Ayre*
+* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets
+ are part of the URI structure, not the actual host.
-* `.unscope` now removes conditions specified in `default_scope`.
+ Fixes #15705.
- *Jon Leighton*
+ *Andy Bakun*, *Aaron Stone*
-* Added `ActiveRecord::QueryMethods#rewhere` which will overwrite an existing, named where condition.
+* Ensure both parent IDs are set on join records when both sides of a
+ through association are new.
- Examples:
+ *Sean Griffin*
- 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
+* `ActiveRecord::Dirty` now detects in-place changes to mutable values.
+ Serialized attributes on ActiveRecord models will no longer save when
+ unchanged.
- *DHH*
+ Fixes #8328.
-* Extend `ActiveRecord::Base#cache_key` to take an optional list of timestamp attributes of which the highest will be used.
+ *Sean Griffin*
- Example:
+* Pluck now works when selecting columns from different tables with the same
+ name.
- # last_reviewed_at will be used, if that's more recent than updated_at, or vice versa
- Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ Fixes #15649.
- *DHH*
+ *Sean Griffin*
-* Added `ActiveRecord::Base#enum` for declaring enum attributes where the values map to integers in the database, but can be queried by name.
+* Remove `cache_attributes` and friends. All attributes are cached.
- Example:
+ *Sean Griffin*
- class Conversation < ActiveRecord::Base
- enum status: [:active, :archived]
- end
-
- Conversation::STATUS # => { active: 0, archived: 1 }
-
- # conversation.update! status: 0
- conversation.active!
- conversation.active? # => true
- conversation.status # => "active"
-
- # conversation.update! status: 1
- conversation.archived!
- conversation.archived? # => true
- conversation.status # => "archived"
+* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`.
- # conversation.update! status: 1
- conversation.status = :archived
+ *Akshay Vishnoi*
- *DHH*
+* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as
+ `Enumerable#find` does.
-* `ActiveRecord::Base#attribute_for_inspect` now truncates long arrays (more than 10 elements).
+ Fixes #15382.
- *Jan Bernacki*
+ *James Yang*
-* Allow for the name of the `schema_migrations` table to be configured.
+* Make timezone aware attributes work with PostgreSQL array columns.
- *Jerad Phelps*
+ Fixes #13402.
-* Do not add to scope includes values from through associations.
- Fixed bug when providing `includes` in through association scope, and fetching targets.
+ *Kuldeep Aggarwal*, *Sean Griffin*
- Example:
-
- class Vendor < ActiveRecord::Base
- has_many :relationships, -> { includes(:user) }
- has_many :users, through: :relationships
- end
+* `ActiveRecord::SchemaMigration` has no primary key regardless of the
+ `primary_key_prefix_type` configuration.
- vendor = Vendor.first
+ Fixes #15051.
- # Before
-
- vendor.users.to_a # => Raises exception: not found `:user` for `User`
-
- # After
-
- vendor.users.to_a # => No exception is raised
-
- Fixes #12242, #9517, #10240.
-
- *Paul Nikitochkin*
+ *JoseLuis Torres*, *Yves Senn*
-* Type cast json values on write, so that the value is consistent
- with reading from the database.
-
- Example:
-
- x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar}
-
- # Before:
- x.tags # => {"string" => "foo", :symbol => :bar}
-
- # After:
- x.tags # => {"string" => "foo", "symbol" => "bar"}
-
- *Severin Schoepke*
-
-* `ActiveRecord::Store` works together with PostgreSQL `hstore` columns.
-
- Fixes #12452.
-
- *Yves Senn*
+* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`.
-* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of
- all registered `stored_attributes`. Now every subclass of
- `ActiveRecord::Base` has it's own `Hash`.
+ Fixes #15538.
*Yves Senn*
-* Save `has_one` association when primary key is manually set.
+* Baseclass becomes! subclass.
- Fixes #12302.
+ Before this change, a record which changed its STI type, could not be
+ updated.
- *Lauro Caetano*
-
-* Allow any version of BCrypt when using `has_secure_password`.
-
- *Mike Perham*
-
-* Sub-query generated for `Relation` passed as array condition did not take in account
- bind values and have invalid syntax.
-
- Generate sub-query with inline bind values.
-
- Fixes #12586.
-
- *Paul Nikitochkin*
-
-* Fix a bug where rake db:structure:load crashed when the path contained
- spaces.
-
- *Kevin Mook*
-
-* `ActiveRecord::QueryMethods#unscope` unscopes negative equality
-
- Allows you to call `#unscope` on a relation with negative equality
- operators, i.e. `Arel::Nodes::NotIn` and `Arel::Nodes::NotEqual` that have
- been generated through the use of `where.not`.
-
- *Eric Hankins*
-
-* Raise an exception when model without primary key calls `.find_with_ids`.
+ Fixes #14785.
- *Shimpei Makimoto*
+ *Matthew Draper*, *Earl St Sauver*, *Edo Balvers*
-* Make `Relation#empty?` use `exists?` instead of `count`.
+* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the
+ `proper_table_name` instance method on `ActiveRecord::Migration` instead.
- *Szymon Nowak*
+ *Akshay Vishnoi*
-* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`.
+* Fix regression on eager loading association based on SQL query rather than
+ existing column.
- *Kenta Okamoto*
+ Fixes #15480.
-* `NullRelation#pluck` takes a list of columns
+ *Lauro Caetano*, *Carlos Antonio da Silva*
- The method signature in `NullRelation` was updated to mimic that in
- `Calculations`.
+* Deprecate returning `nil` from `column_for_attribute` when no column exists.
+ It will return a null object in Rails 5.0
- *Derek Prior*
+ *Sean Griffin*
-* `scope_chain` should not be mutated for other reflections.
+* Implemented ActiveRecord::Base#pretty_print to work with PP.
- Currently `scope_chain` uses same array for building different
- `scope_chain` for different associations. During processing
- these arrays are sometimes mutated and because of in-place
- mutation the changed `scope_chain` impacts other reflections.
+ *Ethan*
- Fix is to dup the value before adding to the `scope_chain`.
-
- Fixes #3882.
-
- *Neeraj Singh*
-
-* Prevent the inversed association from being reloaded on save.
-
- Fixes #9499.
-
- *Dmitry Polushkin*
-
-* Generate subquery for `Relation` if it passed as array condition for `where`
- method.
-
- Example:
-
- # Before
- Blog.where('id in (?)', Blog.where(id: 1))
- # => SELECT "blogs".* FROM "blogs" WHERE "blogs"."id" = 1
- # => SELECT "blogs".* FROM "blogs" WHERE (id IN (1))
-
- # After
- Blog.where('id in (?)', Blog.where(id: 1).select(:id))
- # => SELECT "blogs".* FROM "blogs"
- # WHERE "blogs"."id" IN (SELECT "blogs"."id" FROM "blogs" WHERE "blogs"."id" = 1)
-
- Fixes #12415.
-
- *Paul Nikitochkin*
+* Preserve type when dumping PostgreSQL point, bit, bit varying and money
+ columns.
-* For missed association exception message
- which is raised in `ActiveRecord::Associations::Preloader` class
- added owner record class name in order to simplify to find problem code.
+ *Yves Senn*
- *Paul Nikitochkin*
+* New records remain new after YAML serialization.
-* `has_and_belongs_to_many` is now transparently implemented in terms of
- `has_many :through`. Behavior should remain the same, if not, it is a bug.
+ *Sean Griffin*
-* `create_savepoint`, `rollback_to_savepoint` and `release_savepoint` accept
- a savepoint name.
+* PostgreSQL support default values for enum types. Fixes #7814.
*Yves Senn*
-* Make `next_migration_number` accessible for third party generators.
+* PostgreSQL `default_sequence_name` respects schema. Fixes #7516.
*Yves Senn*
-* Objects instantiated using a null relationship will now retain the
- attributes of the where clause.
+* Fixed `columns_for_distinct` of postgresql adapter to work correctly
+ with orders without sort direction modifiers.
- Fixes #11676, #11675, #11376.
+ *Nikolay Kondratyev*
- *Paul Nikitochkin*, *Peter Brown*, *Nthalk*
+* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719.
-* Fixed `ActiveRecord::Associations::CollectionAssociation#find`
- when using `has_many` association with `:inverse_of` and finding an array of one element,
- it should return an array of one element too.
-
- *arthurnn*
-
-* Callbacks on has_many should access the in memory parent if a inverse_of is set.
-
- *arthurnn*
-
-* `ActiveRecord::ConnectionAdapters.string_to_time` respects
- string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST).
-
- Fixes #12278.
-
- *kennyj*
-
-* 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`.
-
- Example:
-
- @my_comment.update_attributes(nil) # => raises ArgumentError
-
- *John Wang*
-
-* Deprecate `quoted_locking_column` method, which isn't used anywhere.
-
- *kennyj*
-
-* Migration dump UUID default functions to schema.rb.
+ *Yves Senn*
- Fixes #10751.
+* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`.
+ Fixes duplication in combination with `store_accessor`.
- *kennyj*
+ Fixes #15369.
-* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan`
- when using `has_many` association with `:inverse_of` option and UUID primary key.
+ *Yves Senn*
- Fixes #10450.
+* `rake railties:install:migrations` respects the order of railties.
- *kennyj*
+ *Arun Agrawal*
-* Fix: joins association, with defined in the scope block constraints by using several
- where constraints and at least of them is not `Arel::Nodes::Equality`,
- generates invalid SQL expression.
+* Fix redefine a has_and_belongs_to_many inside inherited class
+ Fixing regression case, where redefining the same has_an_belongs_to_many
+ definition into a subclass would raise.
- Fixes #11963.
+ Fixes #14983.
- *Paul Nikitochkin*
+ *arthurnn*
-* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed
- query to fetch results rather than loading the entire collection.
+* Fix has_and_belongs_to_many public reflection.
+ When defining a has_and_belongs_to_many, internally we convert that to two has_many.
+ But as `reflections` is a public API, people expect to see the right macro.
- *Lann Martin*
+ Fixes #14682.
-* Make possible to run SQLite rake tasks without the `Rails` constant defined.
+ *arthurnn*
- *Damien Mathieu*
+* Fixed serialization for records with an attribute named `format`.
-* Allow Relation#from to accept other relations with bind values.
+ Fixes #15188.
- *Ryan Wallace*
+ *Godfrey Chan*
-* Fix inserts with prepared statements disabled.
+* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum`
+ on a NullRelation should return a Hash.
- Fixes #12023.
+ *Kuldeep Aggarwal*
- *Rafael Mendonça França*
+* Fixed serialized fields returning serialized data after being updated with
+ `update_column`.
-* Setting a has_one association on a new record no longer causes an empty
- transaction.
+ *Simon Hørup Eskildsen*
- *Dylan Thacker-Smith*
+* Fixed polymorphic eager loading when using a String as foreign key.
-* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag.
+ Fixes #14734.
- *thedarkone*
+ *Lauro Caetano*
-* Re-use `order` argument pre-processing for `reorder`.
+* Change belongs_to touch to be consistent with timestamp updates
- *Paul Nikitochkin*
+ If a model is set up with a belongs_to: touch relationship the parent
+ record will only be touched if the record was modified. This makes it
+ consistent with timestamp updating on the record itself.
-* Fix PredicateBuilder so polymorphic association keys in `where` clause can
- accept objects other than direct descendants of `ActiveRecord::Base` (decorated
- models, for example).
+ *Brock Trappitt*
- *Mikhail Dieterle*
+* Fixed the inferred table name of a has_and_belongs_to_many auxiliar
+ table inside a schema.
-* PostgreSQL adapter recognizes negative money values formatted with
- parentheses (eg. `($1.25) # => -1.25`)).
- Fixes #11899.
+ Fixes #14824.
- *Yves Senn*
+ *Eric Chahin*
-* Stop interpreting SQL 'string' columns as :string type because there is no
- common STRING datatype in SQL.
+* Remove unused `:timestamp` type. Transparently alias it to `:datetime`
+ in all cases. Fixes inconsistencies when column types are sent outside of
+ `ActiveRecord`, such as for XML Serialization.
- *Ben Woosley*
+ *Sean Griffin*
-* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases.
+* Fix bug that added `table_name_prefix` and `table_name_suffix` to
+ extension names in PostgreSQL when migrating.
- *Xavier Noria*
+ *Joao Carlos*
-* Assign inet/cidr attribute with `nil` value for invalid address.
+* The `:index` option in migrations, which previously was only available for
+ `references`, now works with any column types.
- Example:
+ *Marc Schütz*
- record = User.new
- record.logged_in_from_ip # is type of an inet or a cidr
+* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`.
- # Before:
- record.logged_in_from_ip = 'bad ip address' # raise exception
+ *jnormore*
- # After:
- record.logged_in_from_ip = 'bad ip address' # do not raise exception
- record.logged_in_from_ip # => nil
- record.logged_in_from_ip_before_type_cast # => 'bad ip address'
+* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having`
+ or `offset`.
- *Paul Nikitochkin*
+ In these cases the generated query ignored them and that caused unintended
+ records to be deleted.
-* `add_to_target` now accepts a second optional `skip_callbacks` argument
+ Fixes #11985.
- If truthy, it will skip the :before_add and :after_add callbacks.
+ *Leandro Facchinetti*
- *Ben Woosley*
+* Floats with limit >= 25 that get turned into doubles in MySQL no longer have
+ their limit dropped from the schema.
-* Fix interactions between `:before_add` callbacks and nested attributes
- assignment of `has_many` associations, when the association was not
- yet loaded:
+ Fixes #14135.
- - A `:before_add` callback was being called when a nested attributes
- assignment assigned to an existing record.
+ *Aaron Nelson*
- - Nested Attributes assignment did not affect the record in the
- association target when a `:before_add` callback triggered the
- loading of the association
+* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many
+ association.
- *Jörg Schray*
+ Fixes #14709.
-* Allow enable_extension migration method to be revertible.
+ *Kassio Borges*
- *Eric Tipton*
+* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and
+ strings in column names as equal.
-* Type cast hstore values on write, so that the value is consistent
- with reading from the database.
+ This fixes a rare case in which more bind values are passed than there are
+ placeholders for them in the generated SQL statement, which can make PostgreSQL
+ throw a `StatementInvalid` exception.
- Example:
+ *Nat Budin*
- x = Hstore.new tags: {"bool" => true, "number" => 5}
+* Fix `stored_attributes` to correctly merge the details of stored
+ attributes defined in parent classes.
- # Before:
- x.tags # => {"bool" => true, "number" => 5}
+ Fixes #14672.
- # After:
- x.tags # => {"bool" => "true", "number" => "5"}
+ *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy*
- *Yves Senn* , *Severin Schoepke*
+* `change_column_default` allows `[]` as argument to `change_column_default`.
-* Fix multidimensional PostgreSQL arrays containing non-string items.
+ Fixes #11586.
*Yves Senn*
-* Fixes bug when using includes combined with select, the select statement was overwritten.
-
- Fixes #11773.
-
- *Edo Balvers*
-
-* Load fixtures from linked folders.
-
- *Kassio Borges*
+* Handle `name` and `"char"` column types in the PostgreSQL adapter.
-* Create a directory for sqlite3 file if not present on the system.
+ `name` and `"char"` are special character types used internally by
+ PostgreSQL and are used by internal system catalogs. These field types
+ can sometimes show up in structure-sniffing queries that feature internal system
+ structures or with certain PostgreSQL extensions.
- *Richard Schneeman*
+ *J Smith*, *Yves Senn*
-* Removed redundant override of `xml` column definition for PostgreSQL,
- in order to use `xml` column type instead of `text`.
-
- *Paul Nikitochkin*, *Michael Nikitochkin*
-
-* Revert `ActiveRecord::Relation#order` change that make new order
- prepend the old one.
+* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and
+ NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN`
Before:
- User.order("name asc").order("created_at desc")
- # SELECT * FROM users ORDER BY created_at desc, name asc
+ Point.create(value: 1.0/0)
+ Point.last.value # => 0.0
After:
- User.order("name asc").order("created_at desc")
- # SELECT * FROM users ORDER BY name asc, created_at desc
+ Point.create(value: 1.0/0)
+ Point.last.value # => Infinity
- This also affects order defined in `default_scope` or any kind of associations.
+ *Innokenty Mikhailov*
-* Add ability to define how a class is converted to Arel predicates.
- For example, adding a very vendor specific regex implementation:
+* Allow the PostgreSQL adapter to handle bigserial primary key types again.
- regex_handler = proc do |column, value|
- Arel::Nodes::InfixOperation.new('~', column, value.source)
- end
- ActiveRecord::PredicateBuilder.register_handler(Regexp, regex_handler)
+ Fixes #10410.
- *Sean Griffin & @joannecheng*
+ *Patrick Robertson*
-* Don't allow `quote_value` to be called without a column.
+* Deprecate joining, eager loading and preloading of instance dependent
+ associations without replacement. These operations happen before instances
+ are created. The current behavior is unexpected and can result in broken
+ behavior.
- Some adapters require column information to do their job properly.
- By enforcing the provision of the column for this internal method
- we ensure that those using adapters that require column information
- will always get the proper behavior.
+ Fixes #15024.
- *Ben Woosley*
+ *Yves Senn*
-* When using optimistic locking, `update` was not passing the column to `quote_value`
- to allow the connection adapter to properly determine how to quote the value. This was
- affecting certain databases that use specific column types.
+* Fixed has_and_belongs_to_many's CollectionAssociation size calculation.
- Fixes #6763.
+ has_and_belongs_to_many should fall back to using the normal CollectionAssociation's
+ size calculation if the collection is not cached or loaded.
- *Alfred Wong*
+ Fixes #14913, #14914.
-* rescue from all exceptions in `ConnectionManagement#call`
+ *Fred Wu*
- Fixes #11497.
+* Return a non zero status when running `rake db:migrate:status` and migration table does
+ not exist.
- As `ActiveRecord::ConnectionAdapters::ConnectionManagement` middleware does
- not rescue from Exception (but only from StandardError), the Connection
- Pool quickly runs out of connections when multiple erroneous Requests come
- in right after each other.
+ *Paul B.*
- Rescuing from all exceptions and not just StandardError, fixes this
- behaviour.
+* Add support for module-level `table_name_suffix` in models.
- *Vipul A M*
+ This makes `table_name_suffix` work the same way as `table_name_prefix` when
+ using namespaced models.
-* `change_column` for PostgreSQL adapter respects the `:array` option.
+ *Jenner LaFave*
- *Yves Senn*
+* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0.
-* Remove deprecation warning from `attribute_missing` for attributes that are columns.
+ In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`.
+ In 4.0 series it is delegated to `Array#join`.
- *Arun Agrawal*
+ *Bogdan Gusiev*
-* Remove extra decrement of transaction deep level.
+* Log nil binary column values correctly.
- Fixes #4566.
+ 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.
- *Paul Nikitochkin*
+ *James Coleman*
-* Reset @column_defaults when assigning `locking_column`.
- We had a potential problem. For example:
+* Rails will now pass a custom validation context through to autosave associations
+ in order to validate child associations with the same context.
- class Post < ActiveRecord::Base
- self.column_defaults # if we call this unintentionally before setting locking_column ...
- self.locking_column = 'my_locking_column'
- end
+ Fixes #13854.
- Post.column_defaults["my_locking_column"]
- => nil # expected value is 0 !
+ *Eric Chahin*, *Aaron Nelson*, *Kevin Casey*
- *kennyj*
+* Stringify all variables keys of MySQL connection configuration.
-* Remove extra select and update queries on save/touch/destroy ActiveRecord model
- with belongs to reflection with option `touch: true`.
+ When `sql_mode` variable for MySQL adapters set in configuration as `String`
+ was ignored and overwritten by strict mode option.
- Fixes #11288.
+ Fixes #14895.
*Paul Nikitochkin*
-* Remove deprecated nil-passing to the following `SchemaCache` methods:
- `primary_keys`, `tables`, `columns` and `columns_hash`.
+* Ensure SQLite3 statements are closed on errors.
- *Yves Senn*
+ Fixes #13631.
-* Remove deprecated block filter from `ActiveRecord::Migrator#migrate`.
+ *Timur Alperovich*
- *Yves Senn*
+* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve.
-* Remove deprecated String constructor from `ActiveRecord::Migrator`.
+ *Hector Satre*
- *Yves Senn*
+* 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.
-* Remove deprecated `scope` use without passing a callable object.
+ Fixes #14845.
- *Arun Agrawal*
+ *Kassio Borges*
-* Remove deprecated `transaction_joinable=` in favor of `begin_transaction`
- with `:joinable` option.
+* Reset the cache when modifying a Relation with cached Arel.
+ Additionally display a warning message to make the user aware.
- *Arun Agrawal*
+ *Yves Senn*
-* Remove deprecated `decrement_open_transactions`.
+* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures
+ different spellings of timestamps are treated the same.
- *Arun Agrawal*
+ Example:
-* Remove deprecated `increment_open_transactions`.
+ mytimestamp.simplified_type('timestamp without time zone')
+ # => :datetime
+ mytimestamp.simplified_type('timestamp(6) without time zone')
+ # => also :datetime (previously would be :timestamp)
- *Arun Agrawal*
+ See #14513.
-* Remove deprecated `PostgreSQLAdapter#outside_transaction?`
- method. You can use `#transaction_open?` instead.
+ *Jefferson Lai*
- *Yves Senn*
+* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions.
-* Remove deprecated `ActiveRecord::Fixtures.find_table_name` in favor of
- `ActiveRecord::Fixtures.default_fixture_model_name`.
+ Fixes #14841.
- *Vipul A M*
+ *Lucas Mazza*
-* Removed deprecated `columns_for_remove` from `SchemaStatements`.
+* Fix name collision with `Array#select!` with `Relation#select!`.
- *Neeraj Singh*
+ Fixes #14752.
-* Remove deprecated `SchemaStatements#distinct`.
+ *Earl St Sauver*
- *Francesco Rodriguez*
+* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`.
-* Move deprecated `ActiveRecord::TestCase` into the rails test
- suite. The class is no longer public and is only used for internal
- Rails tests.
+ 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.
- *Yves Senn*
+ Fixes #14537.
-* Removed support for deprecated option `:restrict` for `:dependent`
- in associations.
+ *Jan Habermann*
- *Neeraj Singh*
+* `@destroyed` should always be set to `false` when an object is duped.
-* Removed support for deprecated `delete_sql` in associations.
+ *Kuldeep Aggarwal*
- *Neeraj Singh*
+* Fixed has_many association to make it support irregular inflections.
-* Removed support for deprecated `insert_sql` in associations.
+ Fixes #8928.
- *Neeraj Singh*
+ *arthurnn*, *Javier Goizueta*
-* Removed support for deprecated `finder_sql` in associations.
+* Fixed a problem where count used with a grouping was not returning a Hash.
- *Neeraj Singh*
+ Fixes #14721.
-* Support array as root element in JSON fields.
+ *Eric Chahin*
- *Alexey Noskov & Francesco Rodriguez*
+* `sanitize_sql_like` helper method to escape a string for safe use in an SQL
+ LIKE statement.
-* Removed support for deprecated `counter_sql` in associations.
+ Example:
- *Neeraj Singh*
+ class Article
+ def self.search(term)
+ where("title LIKE ?", sanitize_sql_like(term))
+ end
+ end
-* Do not invoke callbacks when `delete_all` is called on collection.
+ Article.search("20% _reduction_")
+ # => Query looks like "... title LIKE '20\% \_reduction\_' ..."
- Method `delete_all` should not be invoking callbacks and this
- feature was deprecated in Rails 4.0. This is being removed.
- `delete_all` will continue to honor the `:dependent` option. However
- if `:dependent` value is `:destroy` then the `:delete_all` deletion
- strategy for that collection will be applied.
+ *Rob Gilson*, *Yves Senn*
- User can also force a deletion strategy by passing parameter to
- `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`.
+* Do not quote uuid default value on `change_column`.
- *Neeraj Singh*
+ Fixes #14604.
-* Calling default_scope without a proc will now raise `ArgumentError`.
+ *Eric Chahin*
- *Neeraj Singh*
+* The comparison between `Relation` and `CollectionProxy` should be consistent.
-* Removed deprecated method `type_cast_code` from Column.
+ Example:
- *Neeraj Singh*
+ author.posts == Post.where(author_id: author.id)
+ # => true
+ Post.where(author_id: author.id) == author.posts
+ # => true
-* Removed deprecated options `delete_sql` and `insert_sql` from HABTM
- association.
+ Fixes #13506.
- Removed deprecated options `finder_sql` and `counter_sql` from
- collection association.
+ *Lauro Caetano*
- *Neeraj Singh*
+* Calling `delete_all` on an unloaded `CollectionProxy` no longer
+ generates an SQL statement containing each id of the collection:
-* Remove deprecated `ActiveRecord::Base#connection` method.
- Make sure to access it via the class.
+ Before:
- *Yves Senn*
+ DELETE FROM `model` WHERE `model`.`parent_id` = 1
+ AND `model`.`id` IN (1, 2, 3...)
-* Remove deprecation warning for `auto_explain_threshold_in_seconds`.
+ After:
- *Yves Senn*
+ DELETE FROM `model` WHERE `model`.`parent_id` = 1
-* Remove deprecated `:distinct` option from `Relation#count`.
+ *Eileen M. Uchitelle*, *Aaron Patterson*
- *Yves Senn*
+* Fixed error for aggregate methods (`empty?`, `any?`, `count`) with `select`
+ which created invalid SQL.
-* Removed deprecated methods `partial_updates`, `partial_updates?` and
- `partial_updates=`.
+ Fixes #13648.
- *Neeraj Singh*
+ *Simon Woker*
-* Removed deprecated method `scoped`.
+* PostgreSQL adapter only warns once for every missing OID per connection.
- *Neeraj Singh*
+ Fixes #14275.
-* Removed deprecated method `default_scopes?`.
+ *Matthew Draper*, *Yves Senn*
- *Neeraj Singh*
+* PostgreSQL adapter automatically reloads it's type map when encountering
+ unknown OIDs.
-* Remove implicit join references that were deprecated in 4.0.
+ Fixes #14678.
- Example:
+ *Matthew Draper*, *Yves Senn*
- # before with implicit joins
- Comment.where('posts.author_id' => 7)
+* Fix insertion of records via `has_many :through` association with scope.
- # after
- Comment.references(:posts).where('posts.author_id' => 7)
+ Fixes #3548.
- *Yves Senn*
+ *Ivan Antropov*
-* Apply default scope when joining associations. For example:
+* Auto-generate stable fixture UUIDs on PostgreSQL.
- class Post < ActiveRecord::Base
- default_scope -> { where published: true }
- end
+ Fixes #11524.
- class Comment
- belongs_to :post
- end
+ *Roderick van Domburg*
- When calling `Comment.joins(:post)`, we expect to receive only
- comments on published posts, since that is the default scope for
- posts.
+* Fixed a problem where an enum would overwrite values of another enum
+ with the same name in an unrelated class.
- Before this change, the default scope from `Post` was not applied,
- so we'd get comments on unpublished posts.
+ Fixes #14607.
- *Jon Leighton*
+ *Evan Whalen*
-* Remove `activerecord-deprecated_finders` as a dependency.
+* PostgreSQL and SQLite string columns no longer have a default limit of 255.
- *Łukasz Strzałkowski*
+ Fixes #13435, #9153.
-* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0.
+ *Vladimir Sazhin*, *Toms Mikoss*, *Yves Senn*
- *kennyj*
+* Make possible to have an association called `records`.
-* `find_each` now returns an `Enumerator` when called without a block, so that it
- can be chained with other `Enumerable` methods.
+ Fixes #11645.
- *Ben Woosley*
+ *prathamesh-sonpatki*
-* `ActiveRecord::Result.each` now returns an `Enumerator` when called without
- a block, so that it can be chained with other `Enumerable` methods.
+* `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.
- *Ben Woosley*
+ Fixes #14003.
-* Flatten merged join_values before building the joins.
+ *Jefferson Lai*
- 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.
+* Block a few default Class methods as scope name.
- Fixes #10669.
+ For instance, this will raise:
- *Neeraj Singh and iwiznia*
+ scope :public, -> { where(status: 1) }
-* Do not load all child records for inverse case.
+ *arthurnn*
- currently `post.comments.find(Comment.first.id)` would load all
- comments for the given post to set the inverse association.
+* Fixed error when using `with_options` with lambda.
- 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.
+ Fixes #9805.
- Fix is to use in-memory records only if loaded? is true. Otherwise
- load the records using full sql.
+ *Lauro Caetano*
- Fixes #10509.
+* Switch `sqlite3:///` URLs (which were temporarily
+ deprecated in 4.1) from relative to absolute.
- *Neeraj Singh*
+ If you still want the previous interpretation, you should replace
+ `sqlite3:///my/path` with `sqlite3:my/path`.
-* `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.
+ *Matthew Draper*
- Example:
+* Treat blank UUID values as `nil`.
- Author.inspect # => "Author(no database connection)"
+ Example:
- *Yves Senn*
+ Sample.new(uuid_field: '') #=> <Sample id: nil, uuid_field: nil>
-* Handle single quotes in PostgreSQL default column values.
- Fixes #10881.
+ *Dmitry Lavrov*
- *Dylan Markow*
+* Enable support for materialized views on PostgreSQL >= 9.3.
-* Log the sql that is actually sent to the database.
+ *Dave Lee*
- 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'`.
+* The PostgreSQL adapter supports custom domains. Fixes #14305.
- Do not squeeze whitespace out of sql queries. Fixes #10982.
+ *Yves Senn*
- *Neeraj Singh*
+* 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`.
-* Fixture setup no longer depends on `ActiveRecord::Base.configurations`.
- This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`.
+ See #7814.
*Yves Senn*
-* Fix mysql2 adapter raises the correct exception when executing a query on a
- closed connection.
+* Fixed error when specifying a non-empty default value on a PostgreSQL array column.
- *Yves Senn*
+ Fixes #10613.
-* Ambiguous reflections are on :through relationships are no longer supported.
- For example, you need to change this:
+ *Luke Steensen*
- class Author < ActiveRecord::Base
- has_many :posts
- has_many :taggings, through: :posts
- end
+* Fixed error where .persisted? throws SystemStackError for an unsaved model with a
+ custom primary key that didn't save due to validation error.
- class Post < ActiveRecord::Base
- has_one :tagging
- has_many :taggings
- end
+ Fixes #14393.
- class Tagging < ActiveRecord::Base
- end
+ *Chris Finne*
- To this:
+* Introduce `validate` as an alias for `valid?`.
- class Author < ActiveRecord::Base
- has_many :posts
- has_many :taggings, through: :posts, source: :tagging
- end
+ This is more intuitive when you want to run validations but don't care about the return value.
- class Post < ActiveRecord::Base
- has_one :tagging
- has_many :taggings
- end
+ *Henrik Nyh*
- class Tagging < ActiveRecord::Base
- end
+* Create indexes inline in CREATE TABLE for MySQL.
- *Aaron Patterson*
+ This is important, because adding an index on a temporary table after it has been created
+ would commit the transaction.
-* 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.
+ It also allows creating and dropping indexed tables with fewer queries and fewer permissions
+ required.
Example:
- User.select("name, username").count
- # Before => SELECT count(*) FROM users
- # After => ActiveRecord::StatementInvalid
-
- # 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
-
- *Yves Senn*
-
-* 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.
-
- 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.
-
- 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.
-
- You can turn off the automatic detection of inverse associations by setting
- the `:inverse_of` option to `false` like so:
-
- class Taggable < ActiveRecord::Base
- belongs_to :tag, inverse_of: false
+ 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
+
+ *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca*
- *John Wang*
+* Use singular table name in generated migrations when
+ `ActiveRecord::Base.pluralize_table_names` is `false`.
-* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432.
+ Fixes #13426.
- *Adam Anderson*
+ *Kuldeep Aggarwal*
-* Usage of `implicit_readonly` is being removed`. Please use `readonly` method
- explicitly to mark records as `readonly.
- Fixes #10615.
+* `touch` accepts many attributes to be touched at once.
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
+ # touches :signed_at, :sealed_at, and :updated_at/on attributes.
+ Photo.last.touch(:signed_at, :sealed_at)
- *Yves Senn*
+ *James Pinto*
-* Fix the `:primary_key` option for `has_many` associations.
+* `rake db:structure:dump` only dumps schema information if the schema
+ migration table exists.
- Fixes #10693.
+ Fixes #14217.
*Yves Senn*
-* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1.
-
- Fixes #10620.
+* 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.
- *Aaron Patterson*
+ *Matthew Draper*
-* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1.
+* `pk_and_sequence_for` now ensures that only the pg_depend entries
+ pointing to pg_class, and thus only sequence objects, are considered.
- *kennyj*
+ *Josh Williams*
-* Deprecate `ConnectionAdapters::SchemaStatements#distinct`,
- as it is no longer used by internals.
+* `where.not` adds `references` for `includes` like normal `where` calls do.
- *Ben Woosley*
+ Fixes #14406.
-* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix`
- is not blank.
+ *Yves Senn*
- Call `assume_migrated_upto_version` on connection to prevent it from first
- being picked up in `method_missing`.
+* Extend fixture `$LABEL` replacement to allow string interpolation.
- 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`.
+ Example:
- Fixes #10411.
+ martin:
+ email: $LABEL@email.com
- *Kyle Stevens*
+ users(:martin).email # => martin@email.com
-* Method `read_attribute_before_type_cast` should accept input as symbol.
+ *Eric Steele*
- *Neeraj Singh*
+* Add support for `Relation` be passed as parameter on `QueryCache#select_all`.
-* Confirm a record has not already been destroyed before decrementing counter cache.
+ Fixes #14361.
- *Ben Tucker*
+ *arthurnn*
-* 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`.
+* Passing an Active Record object to `find` or `exists?` is now deprecated.
+ Call `.id` on the object first.
- *Zach Ohlgren*
+ *Aaron Patterson*
-* While removing index if column option is missing then raise IrreversibleMigration exception.
+* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation.
- Following code should raise `IrreversibleMigration`. But the code was
- failing since options is an array and not a hash.
+ *Ryuta Kamizono*
- def change
- change_table :users do |t|
- t.remove_index [:name, :email]
- end
- end
+* Support for MySQL 5.6 fractional seconds.
- Fix was to check if the options is a Hash before operating on it.
+ *arthurnn*, *Tatsuhiko Miyagawa*
- Fixes #10419.
+* Support for Postgres `citext` data type enabling case-insensitive where
+ values without needing to wrap in UPPER/LOWER sql functions.
- *Neeraj Singh*
+ *Troy Kruthoff*, *Lachlan Sylvester*
-* Do not overwrite manually built records during one-to-one nested attribute assignment
+* Only save has_one associations if record has changes.
+ Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one
+ object did not get saved to the db.
- 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.:
+ *Alan Kennedy*
- class Member < ActiveRecord::Base
- has_one :avatar
- accepts_nested_attributes_for :avatar
+* Allow strings to specify the `#order` value.
- def avatar
- super || build_avatar(width: 200)
- end
- end
+ Example:
- member = Member.new
- member.avatar_attributes = {icon: 'sad'}
- member.avatar.width # => 200
+ Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql
- *Olek Janiszewski*
+ *Marcelo Casiraghi*, *Robin Dupret*
-* 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.
+* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID"
+ warnings on enum columns.
- *Johnny Holton*
+ *Dieter Komendera*
-* Handle aliased attributes in ActiveRecord::Relation.
+* `includes` is able to detect the right preloading strategy when string
+ joins are involved.
- When using symbol keys, ActiveRecord will now translate aliased attribute names to the actual column name used in the database:
+ Fixes #14109.
- With the model
+ *Aaron Patterson*, *Yves Senn*
- class Topic
- alias_attribute :heading, :title
- end
+* Fixed error with validation with enum fields for records where the
+ value for any enum attribute is always evaluated as 0 during
+ uniqueness validation.
- The call
+ Fixes #14172.
- Topic.where(heading: 'The First Topic')
+ *Vilius Luneckas* *Ahmed AbouElhamayed*
- should yield the same result as
+* `before_add` callbacks are fired before the record is saved on
+ `has_and_belongs_to_many` associations *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.
- Topic.where(title: 'The First Topic')
+ Fixes #14144.
- This also applies to ActiveRecord::Relation::Calculations calls such as `Model.sum(:aliased)` and `Model.pluck(:aliased)`.
+* Fixed STI classes not defining an attribute method if there is a
+ conflicting private method defined on its ancestors.
- This will not work with SQL fragment strings like `Model.sum('DISTINCT aliased')`.
+ Fixes #11569.
*Godfrey Chan*
-* Mute `psql` output when running rake db:schema:load.
+* Coerce strings when reading attributes. Fixes #10485.
- *Godfrey Chan*
+ Example:
-* Trigger a save on `has_one association=(associate)` when the associate contents have changed.
+ book = Book.new(title: 12345)
+ book.save!
+ book.title # => "12345"
- Fixes #8856.
+ *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/README.rdoc b/activerecord/README.rdoc
index e04abe9b37..f4777919d3 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -1,4 +1,4 @@
-= Active Record -- Object-relational mapping put on rails
+= Active Record -- Object-relational mapping in Rails
Active Record connects classes to relational database tables to establish an
almost zero-configuration persistence layer for applications. The library
@@ -20,8 +20,10 @@ A short rundown of some of the major features:
class Product < ActiveRecord::Base
end
- The Product class is automatically mapped to the table named "products",
- which might look like this:
+ {Learn more}[link:classes/ActiveRecord/Base.html]
+
+The Product class is automatically mapped to the table named "products",
+which might look like this:
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
@@ -29,10 +31,8 @@ A short rundown of some of the major features:
PRIMARY KEY (id)
);
- This would also define the following accessors: `Product#name` and
- `Product#name=(new_name)`
-
- {Learn more}[link:classes/ActiveRecord/Base.html]
+This would also define the following accessors: `Product#name` and
+`Product#name=(new_name)`.
* Associations between objects defined by simple class methods.
@@ -130,7 +130,7 @@ A short rundown of some of the major features:
SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html].
-* Logging support for Log4r[http://log4r.rubyforge.org] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
+* Logging support for Log4r[https://github.com/colbygk/log4r] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new('Application Log')
@@ -208,6 +208,11 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 6f8948f987..7769966a22 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -38,33 +38,36 @@ namespace :test do
end
end
+desc 'Build MySQL and PostgreSQL test databases'
namespace :db do
- 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']
+ task :create => ['db:mysql:build', 'db:postgresql:build']
+ task :drop => ['db:mysql:drop', 'db:postgresql:drop']
end
-%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
- Rake::TestTask.new("test_#{adapter}") { |t|
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- t.libs << 'test'
- t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
- |x| x =~ /\/adapters\//
- } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
-
- t.warning = true
- t.verbose = true
- }
-
- task "isolated_test_#{adapter}" do
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- puts [adapter, adapter_short].inspect
- (Dir["test/cases/**/*_test.rb"].reject {
- |x| x =~ /\/adapters\//
- } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, '-w' ,"-Itest", file)
- end or raise "Failures"
+%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
+ namespace :test do
+ Rake::TestTask.new(adapter => "#{adapter}:env") { |t|
+ adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ t.libs << 'test'
+ t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
+ |x| x =~ /\/adapters\//
+ } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
+
+ t.warning = true
+ t.verbose = true
+ }
+
+ namespace :isolated do
+ task adapter => "#{adapter}:env" do
+ adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ puts [adapter, adapter_short].inspect
+ (Dir["test/cases/**/*_test.rb"].reject {
+ |x| x =~ /\/adapters\//
+ } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
+ sh(Gem.ruby, '-w' ,"-Itest", file)
+ end or raise "Failures"
+ end
+ end
end
namespace adapter do
@@ -76,8 +79,8 @@ end
end
# Make sure the adapter test evaluates the env setting task
- task "test_#{adapter}" => "#{adapter}:env"
- task "isolated_test_#{adapter}" => "#{adapter}:env"
+ task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"]
+ task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
end
rule '.sqlite3' do |t|
@@ -89,109 +92,58 @@ task :test_sqlite3 => [
'test/fixtures/fixture_database_2.sqlite3'
]
-namespace :mysql do
- desc 'Build the MySQL test databases'
- task :build_databases do
- config = ARTest.config['connections']['mysql']
- %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- end
-
- desc 'Drop the MySQL test databases'
- task :drop_databases do
- config = ARTest.config['connections']['mysql']
- %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
- %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
- end
-
- desc 'Rebuild the MySQL test databases'
- task :rebuild_databases => [:drop_databases, :build_databases]
-end
-
-task :build_mysql_databases => 'mysql:build_databases'
-task :drop_mysql_databases => 'mysql:drop_databases'
-task :rebuild_mysql_databases => 'mysql:rebuild_databases'
-
-
-namespace :postgresql do
- desc 'Build the PostgreSQL test databases'
- task :build_databases do
- config = ARTest.config['connections']['postgresql']
- %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
- %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
+namespace :db do
+ namespace :mysql do
+ desc 'Build the MySQL test databases'
+ task :build do
+ config = ARTest.config['connections']['mysql']
+ %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ 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"
+ desc 'Drop the MySQL test databases'
+ task :drop do
+ config = ARTest.config['connections']['mysql']
+ %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
+ %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
end
- end
- desc 'Drop the PostgreSQL test databases'
- task :drop_databases do
- config = ARTest.config['connections']['postgresql']
- %x( dropdb #{config['arunit']['database']} )
- %x( dropdb #{config['arunit2']['database']} )
+ desc 'Rebuild the MySQL test databases'
+ task :rebuild => [:drop, :build]
end
- desc 'Rebuild the PostgreSQL test databases'
- task :rebuild_databases => [:drop_databases, :build_databases]
-end
-
-task :build_postgresql_databases => 'postgresql:build_databases'
-task :drop_postgresql_databases => 'postgresql:drop_databases'
-task :rebuild_postgresql_databases => 'postgresql:rebuild_databases'
-
-
-namespace :frontbase do
- desc 'Build the FrontBase test databases'
- task :build_databases => :rebuild_frontbase_databases
-
- desc 'Rebuild the FrontBase test databases'
- task :rebuild_databases do
- build_frontbase_database = Proc.new do |db_name, sql_definition_file|
- %(
- STOP DATABASE #{db_name};
- DELETE DATABASE #{db_name};
- CREATE DATABASE #{db_name};
+ namespace :postgresql do
+ desc 'Build the PostgreSQL test databases'
+ task :build do
+ config = ARTest.config['connections']['postgresql']
+ %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
+ %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
- CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM;
- SET COMMIT FALSE;
-
- CREATE USER RAILS;
- CREATE SCHEMA RAILS AUTHORIZATION RAILS;
- COMMIT;
-
- SET SESSION AUTHORIZATION RAILS;
- SCRIPT '#{sql_definition_file}';
-
- COMMIT;
-
- DISCONNECT ALL;
- )
+ # prepare 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
- config = ARTest.config['connections']['frontbase']
- create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')]
- create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')]
- execute_frontbase_sql = Proc.new do |sql|
- system(<<-SHELL)
- /Library/FrontBase/bin/sql92 <<-SQL
- #{sql}
- SQL
- SHELL
+
+ desc 'Drop the PostgreSQL test databases'
+ task :drop do
+ config = ARTest.config['connections']['postgresql']
+ %x( dropdb #{config['arunit']['database']} )
+ %x( dropdb #{config['arunit2']['database']} )
end
- execute_frontbase_sql[create_activerecord_unittest]
- execute_frontbase_sql[create_activerecord_unittest2]
+
+ desc 'Rebuild the PostgreSQL test databases'
+ task :rebuild => [:drop, :build]
end
end
-task :build_frontbase_databases => 'frontbase:build_databases'
-task :rebuild_frontbase_databases => 'frontbase:rebuild_databases'
+task :build_mysql_databases => 'db:mysql:build'
+task :drop_mysql_databases => 'db:mysql:drop'
+task :rebuild_mysql_databases => 'db:mysql:rebuild'
-spec = eval(File.read('activerecord.gemspec'))
-
-Gem::PackageTask.new(spec) do |p|
- p.gem_spec = spec
-end
+task :build_postgresql_databases => 'db:postgresql:build'
+task :drop_postgresql_databases => 'db:postgresql:drop'
+task :rebuild_postgresql_databases => 'db:postgresql:rebuild'
task :lines do
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
@@ -217,6 +169,11 @@ task :lines do
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
+spec = eval(File.read('activerecord.gemspec'))
+
+Gem::PackageTask.new(spec) do |p|
+ p.gem_spec = spec
+end
# Publishing ------------------------------------------------------
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index d397c9e016..8075008574 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', '~> 5.0.0'
+ s.add_dependency 'arel', '~> 6.0.0'
end
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f856c482d6..9028970a3d 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -27,10 +27,12 @@ require 'active_model'
require 'arel'
require 'active_record/version'
+require 'active_record/attribute_set'
module ActiveRecord
extend ActiveSupport::Autoload
+ autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
@@ -95,6 +97,7 @@ module ActiveRecord
module Coders
autoload :YAMLColumn, 'active_record/coders/yaml_column'
+ autoload :JSON, 'active_record/coders/json'
end
module AttributeMethods
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 0d5313956b..e576ec4d40 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -129,10 +129,10 @@ module ActiveRecord
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
- # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
- # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
- # values can be assigned to the value object using either another NetAddr::CIDR object, a string
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
+ # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
+ # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
+ # New values can be assigned to the value object using either another NetAddr::CIDR object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
@@ -230,8 +230,8 @@ module ActiveRecord
private
def reader_method(name, class_name, mapping, allow_nil, constructor)
define_method(name) do
- if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
- attrs = mapping.collect {|pair| read_attribute(pair.first)}
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? })
+ attrs = mapping.collect {|key, _| read_attribute(key)}
object = constructor.respond_to?(:call) ?
constructor.call(*attrs) :
class_name.constantize.send(constructor, *attrs)
@@ -244,15 +244,19 @@ module ActiveRecord
def writer_method(name, class_name, mapping, allow_nil, converter)
define_method("#{name}=") do |part|
klass = class_name.constantize
+ if part.is_a?(Hash)
+ part = klass.new(*part.values)
+ end
+
unless part.is_a?(klass) || converter.nil? || part.nil?
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
end
if part.nil? && allow_nil
- mapping.each { |pair| self[pair.first] = nil }
+ mapping.each { |key, _| self[key] = nil }
@aggregation_cache[name] = nil
else
- mapping.each { |pair| self[pair.first] = part.send(pair.last) }
+ mapping.each { |key, value| self[key] = part.send(value) }
@aggregation_cache[name] = part.freeze
end
end
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 142d21ce92..830c633c36 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})")
@@ -44,7 +50,7 @@ module ActiveRecord
def initialize(reflection)
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
+ source_associations = reflection.through_reflection.klass._reflections.keys
super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -145,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
@@ -196,12 +202,13 @@ module ActiveRecord
# For instance, +attributes+ and +connection+ would be bad choices for association names.
#
# == Auto-generated methods
+ # See also Instance Public methods below for more details.
#
# === Singular associations (one-to-one)
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
- # other | X | X | X
+ # other(force_reload=false) | X | X | X
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
@@ -211,7 +218,7 @@ module ActiveRecord
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
- # others | X | X | X
+ # others(force_reload=false) | X | X | X
# others=(other,other,...) | X | X | X
# other_ids | X | X | X
# other_ids=(id,id,...) | X | X | X
@@ -413,6 +420,10 @@ module ActiveRecord
# has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
# end
#
+ # Note: Joining, eager loading and preloading of these associations is not fully possible.
+ # These operations happen before instance creation and the scope will be called with a +nil+ argument.
+ # This can lead to unexpected behavior and is deprecated.
+ #
# == Association callbacks
#
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
@@ -530,8 +541,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:
#
@@ -706,9 +717,9 @@ module ActiveRecord
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
+ # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
- # use of eager loading, the 101 queries can be reduced to 2.
+ # use of eager loading, the number of queries will be reduced from 101 to 2.
#
# class Post < ActiveRecord::Base
# belongs_to :author
@@ -767,11 +778,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'
@@ -1036,6 +1052,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 +name+ 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.
@@ -1094,9 +1113,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:
@@ -1115,7 +1131,7 @@ module ActiveRecord
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Options
# [:class_name]
@@ -1211,6 +1227,9 @@ 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 +name+ 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)]
@@ -1229,9 +1248,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:
@@ -1243,7 +1259,7 @@ module ActiveRecord
#
# === Options
#
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# Options are:
# [:class_name]
@@ -1293,6 +1309,10 @@ module ActiveRecord
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
@@ -1304,6 +1324,7 @@ module ActiveRecord
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ # has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1317,6 +1338,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 +name+ 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)]
@@ -1332,9 +1356,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:
@@ -1343,7 +1364,7 @@ module ActiveRecord
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Options
#
@@ -1397,7 +1418,7 @@ module ActiveRecord
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
- # If true, the associated object will be touched (the updated_at/on attributes set to now)
+ # If true, the associated object will be touched (the updated_at/on attributes set to current time)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time in addition to the updated_at/on attribute.
# [:inverse_of]
@@ -1405,18 +1426,23 @@ module ActiveRecord
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
- # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" },
+ # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
# class_name: "Coupon", foreign_key: "coupon_id"
# belongs_to :attachable, polymorphic: true
# belongs_to :project, readonly: true
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
+ # belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
@@ -1454,6 +1480,9 @@ module ActiveRecord
#
# Adds the following methods for retrieval and query:
#
+ # +collection+ is a placeholder for the symbol passed as the +name+ 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.
@@ -1495,9 +1524,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:
@@ -1515,7 +1541,7 @@ module ActiveRecord
# * <tt>Developer#projects.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
- # The declaration may include an options hash to specialize the behavior of the association.
+ # The declaration may include an +options+ hash to specialize the behavior of the association.
#
# === Options
#
@@ -1561,14 +1587,22 @@ module ActiveRecord
scope = nil
end
+ habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
+
builder = Builder::HasAndBelongsToMany.new name, self, options
join_model = builder.through_model
+ # FIXME: we should move this to the internal constants. Also people
+ # should never directly access this constant so I'm not happy about
+ # setting it.
+ const_set join_model.name, join_model
+
middle_reflection = builder.middle_reflection join_model
Builder::HasMany.define_callbacks self, middle_reflection
Reflection.add_reflection self, middle_reflection.name, middle_reflection
+ middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
include Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -1584,11 +1618,12 @@ module ActiveRecord
hm_options[:through] = middle_reflection.name
hm_options[:source] = join_model.right_reflection.name
- [:before_add, :after_add, :before_remove, :after_remove, :autosave].each do |k|
+ [: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
+ self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 85109aee6c..a6a1947148 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -32,8 +32,18 @@ module ActiveRecord
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
- else
+ elsif join.respond_to? :left
join.left.table_name == name ? 1 : 0
+ else
+ # this branch is reached by two tests:
+ #
+ # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
+ # with :posts
+ #
+ # activerecord/test/cases/associations/eager_test.rb:1133
+ # with :comments
+ #
+ 0
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 4e46256862..f1c36cd047 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -160,7 +160,7 @@ module ActiveRecord
def marshal_load(data)
reflection_name, ivars = data
ivars.each { |name, val| instance_variable_set(name, val) }
- @reflection = @owner.class.reflect_on_association(reflection_name)
+ @reflection = @owner.class._reflect_on_association(reflection_name)
end
def initialize_attributes(record) #:nodoc:
@@ -179,7 +179,7 @@ module ActiveRecord
def creation_attributes
attributes = {}
- if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
+ if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]
@@ -232,7 +232,7 @@ module ActiveRecord
# Returns true if record contains the foreign_key
def foreign_key_for?(record)
- record.attributes.has_key? reflection.foreign_key
+ 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 27fd9e35db..519d4d8651 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -1,12 +1,34 @@
module ActiveRecord
module Associations
class AssociationScope #:nodoc:
- INSTANCE = new
-
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
+
def scope(association, connection)
klass = association.klass
reflection = association.reflection
@@ -22,6 +44,23 @@ module ActiveRecord
Arel::Nodes::InnerJoin
end
+ def self.get_bind_values(owner, chain)
+ bvs = []
+ chain.each_with_index do |reflection, i|
+ if reflection == chain.last
+ bvs << reflection.join_id_for(owner)
+ 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 construct_tables(chain, klass, refl, alias_tracker)
@@ -49,10 +88,7 @@ module ActiveRecord
end
def bind_value(scope, column, value, alias_tracker)
- substitute = alias_tracker.connection.substitute_at(
- column, scope.bind_values.length)
- scope.bind_values += [[column, value]]
- substitute
+ @bind_substitution.bind_value scope, column, value, alias_tracker
end
def bind(scope, table_name, column_name, value, tracker)
@@ -69,18 +105,9 @@ module ActiveRecord
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :belongs_to
- if reflection.options[:polymorphic]
- key = reflection.association_primary_key(assoc_klass)
- else
- key = reflection.association_primary_key
- end
-
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
if reflection == chain.last
bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
@@ -120,6 +147,7 @@ module ActiveRecord
end
scope.where_values += item.where_values
+ scope.bind_values += item.bind_values
scope.order_values |= item.order_values
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 8272a5584c..81fdd681de 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -31,6 +31,14 @@ 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?
@@ -51,13 +59,15 @@ module ActiveRecord
end
end
- def decrement_counters
- with_cache_name { |name| decrement_counter name }
+ def decrement_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
+ end
end
- def decrement_counter counter_cache_name
+ def increment_counter(counter_cache_name)
if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
+ klass.increment_counter(counter_cache_name, target_id)
end
end
@@ -82,7 +92,7 @@ module ActiveRecord
# has_one associations.
def invertible_for?(record)
inverse = inverse_reflection_for(record)
- inverse && inverse.macro == :has_one
+ inverse && inverse.has_one?
end
def target_id
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index f085fd1cfd..947d61ee7b 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder
reflection = builder.build(model)
define_accessors model, reflection
define_callbacks model, reflection
+ define_validations model, reflection
builder.define_extensions model
reflection
end
@@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder
end
def self.define_callbacks(model, reflection)
- add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ if dependent = reflection.options[:dependent]
+ check_dependent_options(dependent)
+ add_destroy_callbacks(model, reflection)
+ end
+
Association.extensions.each do |extension|
extension.build model, reflection
end
@@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder
CODE
end
+ def self.define_validations(model, reflection)
+ # noop
+ end
+
def self.valid_dependent_options
raise NotImplementedError
end
private
- 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]}"
+ def self.check_dependent_options(dependent)
+ unless valid_dependent_options.include? dependent
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
end
+ end
+ def self.add_destroy_callbacks(model, reflection)
name = reflection.name
model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 5ccaa55a32..954ea3878a 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -26,28 +26,9 @@ module ActiveRecord::Associations::Builder
private
def self.add_counter_cache_methods(mixin)
- return if mixin.method_defined? :belongs_to_counter_cache_after_create
+ return if mixin.method_defined? :belongs_to_counter_cache_after_update
mixin.class_eval do
- def belongs_to_counter_cache_after_create(reflection)
- if record = send(reflection.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(reflection)
- foreign_key = reflection.foreign_key.to_sym
- unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
- record = send reflection.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(reflection)
foreign_key = reflection.foreign_key
cache_column = reflection.counter_cache_column
@@ -73,14 +54,6 @@ module ActiveRecord::Associations::Builder
def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
- model.after_create lambda { |record|
- record.belongs_to_counter_cache_after_create(reflection)
- }
-
- model.before_destroy lambda { |record|
- record.belongs_to_counter_cache_before_destroy(reflection)
- }
-
model.after_update lambda { |record|
record.belongs_to_counter_cache_after_update(reflection)
}
@@ -130,9 +103,14 @@ module ActiveRecord::Associations::Builder
BelongsTo.touch_record(record, foreign_key, n, touch)
}
- model.after_save callback
+ model.after_save callback, if: :changed?
model.after_touch callback
model.after_destroy callback
end
+
+ def self.add_destroy_callbacks(model, reflection)
+ name = reflection.name
+ model.after_destroy lambda { |o| o.association(name).handle_dependency }
+ 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 e472277374..34a555dfd4 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
@@ -11,11 +11,14 @@ module ActiveRecord::Associations::Builder
end
def join_table
- @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ @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
+
+ def klass
+ @lhs_class.send(:compute_type, @rhs_class_name)
+ end
end
def self.build(lhs_class, name, options)
@@ -60,13 +63,13 @@ module ActiveRecord::Associations::Builder
def self.add_left_association(name, options)
belongs_to name, options
- self.left_reflection = reflect_on_association(name)
+ 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)
+ self.right_reflection = _reflect_on_association(rhs_name)
end
}
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 7909b93622..4c8c826f76 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -5,7 +5,7 @@ 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 self.valid_dependent_options
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index f359efd496..c194c8ae9a 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- valid = super + [:order, :as]
+ valid = super + [:as]
valid += [:through, :source, :source_type] if options[:through]
valid
end
@@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder
private
- def self.add_before_destroy_callbacks(model, reflection)
+ def self.add_destroy_callbacks(model, reflection)
super unless reflection.options[:through]
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 e655c389a6..6e6dd7204c 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -3,7 +3,7 @@
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
- super + [:remote, :dependent, :primary_key, :inverse_of]
+ super + [:dependent, :primary_key, :inverse_of, :required]
end
def self.define_accessors(model, reflection)
@@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.define_validations(model, reflection)
+ super
+ if reflection.options[:required]
+ model.validates_presence_of reflection.name
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 03ca00fa70..065a2cff01 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -24,10 +24,6 @@ module ActiveRecord
# If you need to work on all current children, new and existing records,
# +load_target+ and the +loaded+ flag are your friends.
class CollectionAssociation < Association #:nodoc:
- def initialize(owner, reflection)
- super
- @proxy = CollectionProxy.create(klass, self)
- end
# Implements the reader method, e.g. foo.items for Foo.has_many :items
def reader(force_reload = false)
@@ -37,7 +33,7 @@ module ActiveRecord
reload
end
- @proxy
+ @proxy ||= CollectionProxy.create(klass, self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -59,9 +55,9 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_column = reflection.primary_key_column
+ pk_type = reflection.primary_key_type
ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_column.type_cast(i) }
+ ids.map! { |i| pk_type.type_cast_from_user(i) }
replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
end
@@ -138,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) }
@@ -187,11 +182,11 @@ module ActiveRecord
#
# See delete for more info.
def delete_all(dependent = nil)
- if dependent.present? && ![:nullify, :delete_all].include?(dependent)
+ if dependent && ![:nullify, :delete_all].include?(dependent)
raise ArgumentError, "Valid values are :nullify or :delete_all"
end
- dependent = if dependent.present?
+ dependent = if dependent
dependent
elsif options[:dependent] == :destroy
:delete_all
@@ -199,7 +194,7 @@ module ActiveRecord
options[:dependent]
end
- delete(:all, dependent: dependent).tap do
+ delete_or_nullify_all_records(dependent).tap do
reset
loaded!
end
@@ -249,19 +244,12 @@ 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)
+ return if records.empty?
_options = records.extract_options!
dependent = _options[:dependent] || options[:dependent]
- if records.first == :all
- if loaded? || dependent == :destroy
- delete_or_destroy(load_target, dependent)
- else
- delete_records(:all, dependent)
- end
- else
- records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
- delete_or_destroy(records, dependent)
- end
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
+ delete_or_destroy(records, dependent)
end
# Deletes the +records+ and removes them from this association calling
@@ -270,6 +258,7 @@ module ActiveRecord
# Note that this method removes records from the database ignoring the
# +:dependent+ option.
def destroy(*records)
+ return if records.empty?
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
@@ -363,7 +352,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
@@ -372,7 +363,7 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- loaded? ? target.include?(record) : scope.exists?(record)
+ loaded? ? target.include?(record) : scope.exists?(record.id)
end
else
false
@@ -415,9 +406,23 @@ module ActiveRecord
end
private
+ def get_records
+ return scope.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)
+ }
+ end
+
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
def find_target
- records = scope.to_a
+ records = get_records
records.each { |record| set_inverse_instance(record) }
records
end
@@ -452,13 +457,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|
@@ -517,13 +522,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
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index eba688866c..84c8cfe72b 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -357,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
@@ -435,11 +435,6 @@ 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
@@ -860,6 +855,10 @@ module ActiveRecord
!!@association.include?(record)
end
+ def arel
+ scope.arel
+ end
+
def proxy_association
@association
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 6457182195..79c3d2b0f5 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -41,6 +41,14 @@ module ActiveRecord
end
end
+ def empty?
+ if has_cached_counter?
+ size.zero?
+ else
+ super
+ end
+ end
+
private
# Returns the number of records in this collection.
@@ -58,7 +66,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)
+ owner.read_attribute cached_counter_attribute_name
else
scope.count
end
@@ -71,18 +79,29 @@ 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())
+ update_counter_in_database(difference, reflection)
+ update_counter_in_memory(difference, reflection)
+ end
+
+ def update_counter_in_database(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
+ end
+ end
+
+ def update_counter_in_memory(difference, reflection = reflection())
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.changed_attributes.delete(counter) # eww
end
@@ -98,30 +117,39 @@ 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_updates_counter_named?(counter_name, reflection)
+ end
+
+ def inverse_updates_counter_named?(counter_name, reflection = reflection())
+ reflection.klass._reflections.values.any? { |inverse_reflection|
+ inverse_reflection.belongs_to? &&
inverse_reflection.counter_cache_column == counter_name
}
end
+ def delete_count(method, scope)
+ if method == :delete_all
+ scope.delete_all
+ else
+ scope.update_all(reflection.foreign_key => nil)
+ end
+ end
+
+ def delete_or_nullify_all_records(method)
+ count = delete_count(method, self.scope)
+ update_counter(-count)
+ end
+
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- if records == :all || !reflection.klass.primary_key
- scope = self.scope
- else
- scope = self.scope.where(reflection.klass.primary_key => records)
- end
-
- if method == :delete_all
- update_counter(-scope.delete_all)
- else
- update_counter(-scope.update_all(reflection.foreign_key => nil))
- end
+ scope = self.scope.where(reflection.klass.primary_key => records)
+ update_counter(-delete_count(method, scope))
end
end
@@ -132,6 +160,25 @@ module ActiveRecord
false
end
end
+
+ def concat_records(records, *)
+ update_counter_if_success(super, records.length)
+ end
+
+ def _create_record(attributes, *)
+ if attributes.is_a?(Array)
+ super
+ else
+ update_counter_if_success(super, 1)
+ end
+ end
+
+ def update_counter_if_success(saved_successfully, difference)
+ if saved_successfully
+ update_counter_in_memory(difference)
+ end
+ saved_successfully
+ 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 31b8d27892..44c4436e95 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Has Many Through Association
module Associations
@@ -12,17 +11,18 @@ module ActiveRecord
@through_association = nil
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
- # loaded and calling collection.size if it has. If it's more likely than not that the collection does
- # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
- # SELECT query if you use #length.
+ # Returns the size of the collection by executing a SELECT COUNT(*) query
+ # if the collection hasn't been loaded, and by calling collection.size if
+ # it has. If the collection will likely have a size greater than zero,
+ # and if fetching the collection will be needed afterwards, one less
+ # SELECT query will be generated by using #length instead.
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
- count
+ super
end
end
@@ -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|
@@ -63,7 +62,15 @@ module ActiveRecord
end
save_through_record(record)
- update_counter(1)
+ if has_cached_counter? && !through_reflection_updates_counter_cache?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Automatic updating of counter caches on through associations has been
+ deprecated, and will be removed in Rails 5.0. Instead, please set the
+ appropriate counter_cache options on the has_many and belongs_to for
+ your associations to #{through_reflection.name}.
+ MESSAGE
+ update_counter_in_database(1)
+ end
record
end
@@ -73,23 +80,31 @@ module ActiveRecord
@through_association ||= owner.association(through_reflection.name)
end
- # We temporarily cache through record that has been build, because if we build a
- # through record in build_record and then subsequently call insert_record, then we
- # want to use the exact same object.
+ # The through record (built with build_record) is temporarily cached
+ # so that it may be reused if insert_record is subsequently called.
#
- # However, after insert_record has been called, we clear the cache entry because
- # we want it to be possible to have multiple instances of the same record in an
- # association
+ # However, after insert_record has been called, the cache is cleared in
+ # order to allow multiple instances of the same record in an association.
def build_through_record(record)
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build
+ through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
through_record
end
end
+ def options_for_through_record
+ [through_scope_attributes]
+ end
+
+ def through_scope_attributes
+ scope.where_values_hash(through_association.reflection.name.to_s).
+ except!(through_association.reflection.foreign_key,
+ through_association.reflection.klass.inheritance_column)
+ end
+
def save_through_record(record)
build_through_record(record).save!
ensure
@@ -103,9 +118,9 @@ module ActiveRecord
inverse = source_reflection.inverse_of
if inverse
- if inverse.macro == :has_many
+ if inverse.collection?
record.send(inverse.name) << build_through_record(record)
- elsif inverse.macro == :has_one
+ elsif inverse.has_one?
record.send("#{inverse.name}=", build_through_record(record))
end
end
@@ -114,7 +129,7 @@ module ActiveRecord
end
def target_reflection_has_associated_record?
- !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
+ !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?)
end
def update_through_counter?(method)
@@ -128,13 +143,13 @@ module ActiveRecord
end
end
+ def delete_or_nullify_all_records(method)
+ delete_records(load_target, method)
+ end
+
def delete_records(records, method)
ensure_not_nested
- # This is unoptimised; it will load all the target records
- # even when we just want to delete everything.
- records = load_target if records == :all
-
scope = through_association.scope
scope.where! construct_join_attributes(*records)
@@ -168,7 +183,7 @@ module ActiveRecord
klass.decrement_counter counter, records.map(&:id)
end
- if through_reflection.macro == :has_many && update_through_counter?(method)
+ if through_reflection.collection? && update_through_counter?(method)
update_counter(-count, through_reflection)
end
@@ -178,14 +193,18 @@ module ActiveRecord
def through_records_for(record)
attributes = construct_join_attributes(record)
candidates = Array.wrap(through_association.target)
- candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
+ candidates.find_all do |c|
+ attributes.all? do |key, value|
+ c.public_send(key) == value
+ end
+ end
end
def delete_through_records(records)
records.each do |record|
through_records = through_records_for(record)
- if through_reflection.macro == :has_many
+ if through_reflection.collection?
through_records.each { |r| through_association.target.delete(r) }
else
if through_records.include?(through_association.target)
@@ -199,13 +218,18 @@ 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
def invertible_for?(record)
false
end
+
+ def through_reflection_updates_counter_cache?
+ counter_name = cached_counter_attribute_name
+ inverse_updates_counter_named?(counter_name, through_reflection)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 944caacab6..e6095d84dc 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 94f69d4c2d..ec5c189cd3 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -131,7 +131,6 @@ module ActiveRecord
def instantiate(result_set, aliases)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
- type_caster = result_set.column_type primary_key
seen = Hash.new { |h,parent_klass|
h[parent_klass] = Hash.new { |i,parent_id|
@@ -144,8 +143,7 @@ module ActiveRecord
column_aliases = aliases.column_aliases join_root
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)
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
@@ -164,17 +162,17 @@ module ActiveRecord
def make_outer_joins(parent, child)
tables = table_aliases_for(parent, child)
join_type = Arel::Nodes::OuterJoin
- joins = make_constraints parent, child, tables, join_type
+ info = make_constraints parent, child, tables, join_type
- joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
end
def make_inner_joins(parent, child)
tables = child.tables
join_type = Arel::Nodes::InnerJoin
- joins = make_constraints parent, child, tables, join_type
+ info = make_constraints parent, child, tables, join_type
- joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
end
def table_aliases_for(parent, node)
@@ -207,7 +205,7 @@ module ActiveRecord
end
def find_reflection(klass, name)
- klass.reflect_on_association(name) or
+ klass._reflect_on_association(name) or
raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
end
@@ -215,8 +213,9 @@ module ActiveRecord
associations.map do |name, right|
reflection = find_reflection base_klass, name
reflection.check_validity!
+ reflection.check_eager_loadable!
- if reflection.options[:polymorphic]
+ if reflection.polymorphic?
raise EagerLoadPolymorphicError.new(reflection)
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 cee3c9999f..c3bbdccad8 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -21,8 +21,11 @@ module ActiveRecord
super && reflection == other.reflection
end
+ JoinInformation = Struct.new :joins, :binds
+
def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
joins = []
+ bind_values = []
tables = tables.reverse
scope_chain_index = 0
@@ -34,14 +37,9 @@ module ActiveRecord
table = tables.shift
klass = reflection.klass
- case reflection.source_macro
- when :belongs_to
- key = reflection.association_primary_key
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ join_keys = reflection.join_keys(klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
@@ -54,27 +52,33 @@ module ActiveRecord
end
scope_chain_index += 1
- scope_chain_items.concat [klass.send(:build_default_scope)].compact
+ 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
- if reflection.type
- constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name
- end
-
if rel && !rel.arel.constraints.empty?
+ bind_values.concat rel.bind_values
constraint = constraint.and rel.arel.constraints
end
+ if reflection.type
+ value = foreign_klass.base_class.name
+ column = klass.columns_hash[column.to_s]
+
+ substitute = klass.connection.substitute_at(column, bind_values.length)
+ bind_values.push [column, value]
+ constraint = constraint.and table[reflection.type].eq substitute
+ end
+
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, klass
end
- joins
+ JoinInformation.new joins, bind_values
end
# Builds equality condition.
@@ -86,7 +90,7 @@ module ActiveRecord
# end
#
# If I execute `Physician.joins(:appointments).to_a` then
- # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
+ # klass # => Physician
# table # => #<Arel::Table @name="appointments" ...>
# key # => physician_id
# foreign_table # => #<Arel::Table @name="physicians" ...>
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 83637a0409..7519fec10a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -80,7 +80,7 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
- NULL_RELATION = Struct.new(:values).new({})
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact.uniq
@@ -112,13 +112,14 @@ module ActiveRecord
end
def preloaders_for_hash(association, records, scope)
- parent, child = association.to_a.first # hash should only be of length 1
+ association.flat_map { |parent, child|
+ loaders = preloaders_for_one parent, records, scope
- 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
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
+ loaders
}
end
@@ -140,36 +141,14 @@ module ActiveRecord
end
def grouped_records(association, records)
- reflection_records = records_by_reflection(association, records)
-
- reflection_records.each_with_object({}) do |(reflection, r_records),h|
- h[reflection] = r_records.group_by { |record|
- association_klass(reflection, record)
- }
- end
- end
-
- def records_by_reflection(association, records)
- records.group_by do |record|
- reflection = record.class.reflect_on_association(association)
-
- reflection || raise_config_error(record, association)
- end
- end
-
- def raise_config_error(record, association)
- raise ActiveRecord::ConfigurationError,
- "Association named '#{association}' was not found on #{record.class.name}; " \
- "perhaps you misspelled it?"
- end
-
- def association_klass(reflection, record)
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.read_attribute(reflection.foreign_type.to_s)
- klass && klass.constantize
- else
- reflection.klass
+ h = {}
+ records.each do |record|
+ next unless record
+ assoc = record.association(association)
+ klasses = h[assoc.reflection] ||= {}
+ (klasses[assoc.klass] ||= []) << record
end
+ h
end
class AlreadyLoaded
@@ -198,6 +177,7 @@ module ActiveRecord
if owners.first.association(reflection.name).loaded?
return AlreadyLoaded
end
+ reflection.check_preloadable!
case reflection.macro
when :has_many
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 69b65982b3..c0639742be 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -57,9 +57,15 @@ module ActiveRecord
end
def owners_by_key
- @owners_by_key ||= owners.group_by do |owner|
- owner[owner_key_name]
- end
+ @owners_by_key ||= if key_conversion_required?
+ owners.group_by do |owner|
+ owner[owner_key_name].to_s
+ end
+ else
+ owners.group_by do |owner|
+ owner[owner_key_name]
+ end
+ end
end
def options
@@ -93,13 +99,28 @@ module ActiveRecord
records_by_owner
end
+ def key_conversion_required?
+ association_key_type != owner_key_type
+ end
+
+ def association_key_type
+ @klass.type_for_attribute(association_key_name.to_s).type
+ end
+
+ def owner_key_type
+ @model.type_for_attribute(owner_key_name.to_s).type
+ end
+
def load_slices(slices)
@preloaded_records = slices.flat_map { |slice|
records_for(slice)
}
@preloaded_records.map { |record|
- [record, record[association_key_name]]
+ key = record[association_key_name]
+ key = key.to_s if key_conversion_required?
+
+ [record, key]
}
end
@@ -111,12 +132,15 @@ module ActiveRecord
scope = klass.unscoped
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
@@ -127,6 +151,10 @@ module ActiveRecord
end
end
+ if preload_values[:readonly] || values[:readonly]
+ scope.readonly!
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 2a8530af62..1fed7f74e7 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -23,7 +23,7 @@ module ActiveRecord
reset_association owners, through_reflection.name
- middle_records = through_records.map { |(_,rec)| rec }.flatten
+ middle_records = through_records.flat_map { |(_,rec)| rec }
preloaders = preloader.preload(middle_records,
source_reflection.name,
@@ -84,7 +84,7 @@ module ActiveRecord
end
scope.references! reflection_scope.values[:references]
- scope.order! reflection_scope.values[:order] if scope.eager_loading?
+ scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
end
scope
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 399aff378a..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,8 +38,23 @@ 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
- if record = scope.take
+ if record = get_records.first
set_inverse_instance record
end
end
@@ -52,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 ba7d2a3782..611d471e62 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module Associations
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, :chain, :to => :reflection
+ delegate :source_reflection, :through_reflection, :to => :reflection
protected
@@ -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.drop(1).each do |reflection|
+ reflection.chain.drop(1).each do |reflection|
+ relation = reflection.klass.all
+ relation.merge!(reflection.scope) if reflection.scope
+
scope.merge!(
- reflection.klass.all.
- except(:select, :create_with, :includes, :preload, :joins, :eager_load)
+ relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
scope
@@ -39,12 +41,16 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
- }
+ if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ join_attributes = { source_reflection.name => records }
+ else
+ join_attributes = {
+ source_reflection.foreign_key =>
+ records.map { |record|
+ record.send(source_reflection.association_primary_key(reflection.klass))
+ }
+ }
+ end
if options[:source_type]
join_attributes[source_reflection.foreign_type] =
@@ -61,18 +67,17 @@ module ActiveRecord
# Note: this does not capture all cases, for example it would be crazy to try to
# properly support stale-checking for nested associations.
def stale_state
- if through_reflection.macro == :belongs_to
+ if through_reflection.belongs_to?
owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
end
end
def foreign_key_present?
- through_reflection.macro == :belongs_to &&
- !owner[through_reflection.foreign_key].nil?
+ through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil?
end
def ensure_mutable
- if source_reflection.macro != :belongs_to
+ unless source_reflection.belongs_to?
raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
new file mode 100644
index 0000000000..6d38224830
--- /dev/null
+++ b/activerecord/lib/active_record/attribute.rb
@@ -0,0 +1,120 @@
+module ActiveRecord
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type)
+ FromUser.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
+ @value
+ end
+
+ def value_for_database
+ type.type_cast_for_database(value)
+ end
+
+ def changed_from?(old_value)
+ type.changed?(old_value, value, value_before_type_cast)
+ end
+
+ def changed_in_place_from?(old_value)
+ type.changed_in_place?(old_value, value)
+ end
+
+ def with_value_from_user(value)
+ self.class.from_user(name, value, type)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def type_cast
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ protected
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_database(value)
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_user(value)
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type::Value.new)
+ end
+
+ def value
+ nil
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+ end
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 30fa2c8ba5..2887db3bf7 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -11,6 +11,15 @@ module ActiveRecord
# If the passed hash responds to <tt>permitted?</tt> method and the return value
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
+ #
+ # cat = Cat.new(name: "Gorby", status: "yawning")
+ # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
+ # cat.assign_attributes(status: "sleeping")
+ # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
+ #
+ # New attributes will be persisted in the database when the object is saved.
+ #
+ # Aliased to <tt>attributes=</tt>.
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."
@@ -106,7 +115,7 @@ module ActiveRecord
end
class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :column
+ attr_reader :object, :name, :values, :cast_type
def initialize(object, name, values)
@object = object
@@ -117,22 +126,22 @@ module ActiveRecord
def read_value
return if values.values.compact.empty?
- @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
- klass = column.klass
+ @cast_type = object.type_for_attribute(name)
+ klass = cast_type.klass
if klass == Time
read_time
elsif klass == Date
read_date
else
- read_other(klass)
+ read_other
end
end
private
def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
Time.zone.local(*set_values)
else
Time.send(object.class.default_timezone, *set_values)
@@ -140,9 +149,9 @@ module ActiveRecord
end
def read_time
- # If column is a :time (and not :date or :timestamp) there is no need to validate if
+ # If column is a :time (and not :date or :datetime) there is no need to validate if
# there are year/month/day fields
- if column.type == :time
+ if cast_type.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
@@ -172,13 +181,12 @@ module ActiveRecord
end
end
- def read_other(klass)
+ def read_other
max_position = extract_max_param
positions = (1..max_position)
validate_required_parameters!(positions)
- set_values = values.values_at(*positions)
- klass.new(*set_values)
+ values.slice(*positions)
end
# Checks whether some blank date parameter exists. Note that this is different
diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
new file mode 100644
index 0000000000..5b96623b6e
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module AttributeDecorators # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
+ self.attribute_type_decorations = TypeDecorator.new
+ end
+
+ module ClassMethods # :nodoc:
+ def decorate_attribute_type(column_name, decorator_name, &block)
+ matcher = ->(name, _) { name == column_name.to_s }
+ key = "_#{column_name}_#{decorator_name}"
+ decorate_matching_attribute_types(matcher, key, &block)
+ end
+
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
+ clear_caches_calculated_from_columns
+ decorator_name = decorator_name.to_s
+
+ # Create new hashes so we don't modify parent classes
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
+ end
+
+ private
+
+ def add_user_provided_columns(*)
+ super.map do |column|
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
+ column.with_type(decorated_type)
+ end
+ end
+ end
+
+ class TypeDecorator # :nodoc:
+ delegate :clear, to: :@decorations
+
+ def initialize(decorations = {})
+ @decorations = decorations
+ end
+
+ def merge(*args)
+ TypeDecorator.new(@decorations.merge(*args))
+ end
+
+ def apply(name, type)
+ decorations = decorators_for(name, type)
+ decorations.inject(type) do |new_type, block|
+ block.call(new_type)
+ end
+ end
+
+ private
+
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
+ end
+
+ def matching(name, type)
+ @decorations.values.select do |(matcher, _)|
+ matcher.call(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 9326c9c117..a2bb78dfcc 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -18,6 +18,8 @@ module ActiveRecord
include TimeZoneConversion
include Dirty
include Serialization
+
+ delegate :column_for_attribute, to: :class
end
AttrNames = Module.new {
@@ -29,6 +31,8 @@ module ActiveRecord
end
}
+ BLACKLISTED_CLASS_METHODS = %w(private public protected)
+
class AttributeMethodCache
def initialize
@module = Module.new
@@ -46,7 +50,11 @@ module ActiveRecord
end
private
- def method_body; raise NotImplementedError; end
+
+ # Override this method in the subclasses for method body.
+ def method_body(method_name, const_name)
+ raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method."
+ end
end
module ClassMethods
@@ -64,7 +72,8 @@ module ActiveRecord
# 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
+ return false if @attribute_methods_generated
+ # Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
@@ -106,7 +115,8 @@ 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
@@ -131,7 +141,7 @@ module ActiveRecord
# 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)
- class_method_defined_within?(method_name, Base)
+ 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
@@ -146,16 +156,6 @@ 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.
#
@@ -184,23 +184,29 @@ module ActiveRecord
[]
end
end
- end
- # 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:
- 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
- send(method, *args, &block)
+ # Returns the column object for the named attribute.
+ # Returns nil if the named attribute does not exist.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
+ # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
+ #
+ # person.column_for_attribute(:nothing)
+ # # => nil
+ def column_for_attribute(name)
+ column = columns_hash[name.to_s]
+ if column.nil?
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ `column_for_attribute` will return a null object for non-existent columns
+ in Rails 5.0. Use `has_attribute?` if you need to check for an
+ attribute's existence.
+ MESSAGE
end
- else
- super
+ column
end
end
@@ -221,18 +227,14 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
+ return false unless super
name = name.to_s
- self.class.define_attribute_methods
- result = super
-
- # If the result is false the answer is false.
- return false unless result
# If the result is true then check for the select case.
# 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.any? && self.class.column_names.include?(name)
+ if defined?(@attributes) && self.class.column_names.include?(name)
return has_attribute?(name)
end
@@ -249,7 +251,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
+ @attributes.key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
@@ -273,14 +275,7 @@ module ActiveRecord
# person.attributes
# # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
- attribute_names.each_with_object({}) { |name, attrs|
- attrs[name] = read_attribute(name)
- }
- end
-
- # Placeholder so it can be overriden when needed by serialization
- def attributes_for_coder # :nodoc:
- attributes
+ @attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
@@ -323,39 +318,24 @@ module ActiveRecord
# class Task < ActiveRecord::Base
# end
#
- # person = Task.new(title: '', is_done: false)
- # person.attribute_present?(:title) # => false
- # person.attribute_present?(:is_done) # => true
- # person.name = 'Francesco'
- # person.is_done = true
- # person.attribute_present?(:title) # => true
- # person.attribute_present?(:is_done) # => true
+ # task = Task.new(title: '', is_done: false)
+ # task.attribute_present?(:title) # => false
+ # task.attribute_present?(:is_done) # => true
+ # task.title = 'Buy milk'
+ # task.is_done = true
+ # task.attribute_present?(:title) # => true
+ # task.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
value = read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
- # Returns the column object for the named attribute. Returns +nil+ if the
- # named attribute not exists.
- #
- # class Person < ActiveRecord::Base
- # end
- #
- # person = Person.new
- # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
- # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
- #
- # person.column_for_attribute(:nothing)
- # # => nil
- def column_for_attribute(name)
- # FIXME: should this return a null object for columns that don't exist?
- self.class.columns_hash[name.to_s]
- 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 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.
#
+ # Note: +:id+ is always present.
+ #
# Alias for the <tt>read_attribute</tt> method.
#
# class Person < ActiveRecord::Base
@@ -389,13 +369,6 @@ module ActiveRecord
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
- attribute_names.each do |name|
- attributes[name] = clone_attribute_value(reader_method, name)
- end
- attributes
- end
-
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
@@ -413,7 +386,7 @@ module ActiveRecord
def attribute_method?(attr_name) # :nodoc:
# We check defined? because Syck calls respond_to? before actually calling initialize.
- defined?(@attributes) && @attributes.include?(attr_name)
+ defined?(@attributes) && @attributes.key?(attr_name)
end
private
@@ -432,16 +405,16 @@ module ActiveRecord
# Filters the primary keys and readonly attributes from the attribute names.
def attributes_for_update(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !readonly_attribute?(name)
+ attribute_names.reject do |name|
+ readonly_attribute?(name)
end
end
# Filters out the primary keys, from the attribute names, when the primary
# key is to be generated (e.g. the id attribute has no value).
def attributes_for_create(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
+ attribute_names.reject do |name|
+ pk_attribute?(name) && id.nil?
end
end
@@ -450,13 +423,10 @@ module ActiveRecord
end
def pk_attribute?(name)
- column_for_attribute(name).primary
+ name == self.class.primary_key
end
def typecasted_attribute_value(name)
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already typecasted, this code
- # could be simplified
read_attribute(name)
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index f596a8b02e..fd61febd57 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -43,7 +43,7 @@ module ActiveRecord
# task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
# task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
- @attributes[attr_name.to_s]
+ @attributes[attr_name.to_s].value_before_type_cast
end
# Returns a hash of attributes before typecasting and deserialization.
@@ -57,7 +57,7 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
- @attributes
+ @attributes.values_before_type_cast
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 8a1b199997..b58295a106 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -34,28 +34,47 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- reset_changes
+ clear_changes_information
end
end
- def initialize_dup(other) # :nodoc:
- super
- init_changed_attributes
- end
+ def initialize_dup(other) # :nodoc:
+ super
+ calculate_changes_from_defaults
+ end
+
+ def changed?
+ super || changed_in_place.any?
+ end
+
+ def changed
+ super | changed_in_place
+ end
- private
- def initialize_internals_callback
+ def attribute_changed?(attr_name, options = {})
+ result = super
+ # We can't change "from" something in place. Only setters can define
+ # "from" and "to"
+ result ||= changed_in_place?(attr_name) unless options.key?(:from)
+ result
+ end
+
+ def changes_applied
+ super
+ store_original_raw_attributes
+ end
+
+ def clear_changes_information
super
- init_changed_attributes
+ original_raw_attributes.clear
end
- def init_changed_attributes
+ private
+
+ def calculate_changes_from_defaults
@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])
+ self.class.column_defaults.each do |attr, orig_value|
+ changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
end
end
@@ -63,27 +82,43 @@ module ActiveRecord
def write_attribute(attr, value)
attr = attr.to_s
- save_changed_attribute(attr, value)
+ old_value = old_attribute_value(attr)
- super(attr, value)
+ result = super
+ store_original_raw_attribute(attr)
+ save_changed_attribute(attr, old_value)
+ result
end
- def save_changed_attribute(attr, value)
- # The attribute already has an unsaved change.
+ def raw_write_attribute(attr, value)
+ attr = attr.to_s
+
+ result = super
+ original_raw_attributes[attr] = value
+ result
+ end
+
+ def save_changed_attribute(attr, old_value)
+ if attribute_changed?(attr)
+ changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
+ else
+ changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
+ end
+ end
+
+ def old_attribute_value(attr)
if attribute_changed?(attr)
- old = changed_attributes[attr]
- changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ changed_attributes[attr]
else
- old = clone_attribute_value(:read_attribute, attr)
- changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ clone_attribute_value(:read_attribute, attr)
end
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
@@ -93,34 +128,39 @@ module ActiveRecord
changed
end
- def _field_changed?(attr, old, value)
- if column = column_for_attribute(attr)
- if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||
- changes_from_zero_to_string?(old, value))
- value = nil
- else
- value = column.type_cast(value)
- end
+ def _field_changed?(attr, old_value)
+ @attributes[attr].changed_from?(old_value)
+ end
+
+ def changed_in_place
+ self.class.attribute_names.select do |attr_name|
+ changed_in_place?(attr_name)
end
+ end
- old != value
+ def changed_in_place?(attr_name)
+ old_value = original_raw_attribute(attr_name)
+ @attributes[attr_name].changed_in_place_from?(old_value)
end
- def changes_from_nil_to_empty_string?(column, old, value)
- # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
- # Hence we don't record it as a change if the value changes from nil to ''.
- # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
- # be typecast back to 0 (''.to_i => 0)
- column.null && (old.nil? || old == 0) && value.blank?
+ def original_raw_attribute(attr_name)
+ original_raw_attributes.fetch(attr_name) do
+ read_attribute_before_type_cast(attr_name)
+ end
end
- def changes_from_zero_to_string?(old, value)
- # For columns with old 0 and value non-empty string
- old == 0 && value.is_a?(String) && value.present? && non_zero?(value)
+ def original_raw_attributes
+ @original_raw_attributes ||= {}
end
- def non_zero?(value)
- value !~ /\A0+(\.0+)?\z/
+ def store_original_raw_attribute(attr_name)
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ end
+
+ def store_original_raw_attributes
+ attribute_names.each do |attr|
+ store_original_raw_attribute(attr)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 931209b07b..9bd333bbac 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -15,8 +15,10 @@ module ActiveRecord
# Returns the primary key value.
def id
- sync_with_transaction_state
- read_attribute(self.class.primary_key)
+ if pk = self.class.primary_key
+ sync_with_transaction_state
+ read_attribute(pk)
+ end
end
# Sets the primary key value.
@@ -37,6 +39,12 @@ module ActiveRecord
read_attribute_before_type_cast(self.class.primary_key)
end
+ # Returns the primary key previous value.
+ def id_was
+ sync_with_transaction_state
+ attribute_was(self.class.primary_key)
+ end
+
protected
def attribute_method?(attr_name)
@@ -52,7 +60,7 @@ module ActiveRecord
end
end
- ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
def dangerous_attribute_method?(method_name)
super && !ID_ATTRIBUTE_METHODS.include?(method_name)
@@ -81,12 +89,9 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' if base_name.blank?
-
- case primary_key_prefix_type
- when :table_name
+ if base_name && primary_key_prefix_type == :table_name
base_name.foreign_key(false)
- when :table_name_with_underscore
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index d01e9aea59..10869dfc1e 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# 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.
+ # key the @attributes in read_attribute.
def method_body(method_name, const_name)
<<-EOMETHOD
def #{method_name}
@@ -35,35 +35,22 @@ module ActiveRecord
extend ActiveSupport::Concern
- ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
-
- included do
- class_attribute :attribute_types_cached_by_default, instance_writer: false
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- end
-
module ClassMethods
- # +cache_attributes+ allows you to declare which converted attribute
- # values should be cached. Usually caching only pays off for attributes
- # with expensive conversion methods, like time related columns (e.g.
- # +created_at+, +updated_at+).
- def cache_attributes(*attribute_names)
- cached_attributes.merge attribute_names.map { |attr| attr.to_s }
+ [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
+ define_method method_name do |*|
+ cached_attributes_deprecation_warning(method_name)
+ true
+ end
end
- # Returns the attributes which are cached. By default time related columns
- # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
- def cached_attributes
- @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
- end
+ protected
- # Returns +true+ if the provided attribute is being cached.
- def cache_attribute?(attr_name)
- cached_attributes.include?(attr_name)
+ def cached_attributes_deprecation_warning(method_name)
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ Calling `#{method_name}` is no longer necessary. All attributes are cached.
+ MESSAGE
end
- protected
-
if Module.methods_transplantable?
def define_method_attribute(name)
method = ReaderMethodCache[name]
@@ -89,45 +76,15 @@ module ActiveRecord
end
end
end
-
- private
-
- def cacheable_column?(column)
- if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- ! serialized_attributes.include? column.name
- else
- attribute_types_cached_by_default.include?(column.type)
- end
- end
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 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.
+ def read_attribute(attr_name, &block)
name = attr_name.to_s
- @attributes_cache[name] || @attributes_cache.fetch(name) {
- 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
- }
-
- if self.class.cache_attribute?(name)
- @attributes_cache[name] = column.type_cast(value)
- else
- column.type_cast value
- end
- }
+ name = self.class.primary_key if name == 'id'
+ @attributes.fetch_value(name, &block)
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 67abbbc2a0..264ce2bdfa 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -3,20 +3,7 @@ module ActiveRecord
module Serialization
extend ActiveSupport::Concern
- included do
- # Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
- class_attribute :serialized_attributes, instance_accessor: false
- self.serialized_attributes = {}
- end
-
module ClassMethods
- ##
- # :method: serialized_attributes
- #
- # Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
-
# If you have an attribute that needs to be saved to the database as an
# object, and retrieved as the same object, then specify the name of that
# attribute using this method and it will be handled automatically. The
@@ -30,7 +17,8 @@ module ActiveRecord
# ==== 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
#
@@ -38,141 +26,43 @@ module ActiveRecord
# class User < ActiveRecord::Base
# serialize :preferences
# end
- def serialize(attr_name, class_name = Object)
- include Behavior
-
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
+ #
+ # # 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)
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
+ # to ensure special objects (e.g. Active Record models) are dumped correctly
+ # using the #as_json hook.
+ coder = if class_name_or_coder == ::JSON
+ Coders::JSON
+ elsif [: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
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
- end
-
- class Type # :nodoc:
- def initialize(column)
- @column = column
- end
-
- def type_cast(value)
- if value.state == :serialized
- value.unserialized_value @column.type_cast value.value
- else
- value.unserialized_value
- end
- end
-
- def type
- @column.type
- end
-
- def accessor
- ActiveRecord::Store::IndifferentHashAccessor
- end
- end
-
- class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
- def unserialized_value(v = value)
- state == :serialized ? unserialize(v) : value
- end
-
- def serialized_value
- state == :unserialized ? serialize : value
- end
-
- def unserialize(v)
- self.state = :unserialized
- self.value = coder.load(v)
- end
-
- def serialize
- self.state = :serialized
- self.value = coder.dump(value)
- end
- end
-
- # This is only added to the model when serialize is called, which
- # ensures we do not make things slower when serialization is not used.
- module Behavior # :nodoc:
- extend ActiveSupport::Concern
-
- module ClassMethods # :nodoc:
- def initialize_attributes(attributes, options = {})
- serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
- super(attributes, options)
-
- serialized_attributes.each do |key, coder|
- if attributes.key?(key)
- attributes[key] = Attribute.new(coder, attributes[key], serialized)
- end
- end
-
- attributes
- 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)
- else
- super
- end
- end
-
- def _field_changed?(attr, old, value)
- if self.class.serialized_attributes.include?(attr)
- old != value
- else
- super
- end
- end
-
- def read_attribute_before_type_cast(attr_name)
- if self.class.serialized_attributes.include?(attr_name)
- super.unserialized_value
- else
- super
+ decorate_attribute_type(attr_name, :serialize) do |type|
+ Type::Serialized.new(type, coder)
end
end
- def attributes_before_type_cast
- super.dup.tap do |attributes|
- self.class.serialized_attributes.each_key do |key|
- if attributes.key?(key)
- attributes[key] = attributes[key].unserialized_value
- end
- end
- end
- end
-
- def typecasted_attribute_value(name)
- if self.class.serialized_attributes.include?(name)
- @attributes[name].serialized_value
- else
- 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
+ def serialized_attributes
+ ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
+ `serialized_attributes` is deprecated without replacement, and will
+ be removed in Rails 5.0.
+ WARNING
+ @serialized_attributes ||= Hash[
+ columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
+ [c.name, c.cast_type.coder]
+ }
+ ]
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 f168282ea3..f439bd1ffe 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,18 +1,27 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
- class Type # :nodoc:
- def initialize(column)
- @column = column
+ class TimeZoneConverter < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ convert_time_to_time_zone(super)
end
- def type_cast(value)
- value = @column.type_cast(value)
- value.acts_like?(:time) ? value.in_time_zone : value
+ def type_cast_from_user(value)
+ if value.is_a?(Array)
+ value.map { |v| type_cast_from_user(v) }
+ elsif value.respond_to?(:in_time_zone)
+ value.in_time_zone
+ end
end
- def type
- @column.type
+ def convert_time_to_time_zone(value)
+ if value.is_a?(Array)
+ value.map { |v| convert_time_to_time_zone(v) }
+ elsif value.acts_like?(:time)
+ value.in_time_zone
+ else
+ value
+ end
end
end
@@ -27,31 +36,26 @@ module ActiveRecord
end
module ClassMethods
- protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
- def define_method_attribute=(attr_name)
- if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
- method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}=(time)
- time_with_zone = time.respond_to?(:in_time_zone) ? time.in_time_zone : nil
- previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name})
- write_attribute(:#{attr_name}, time)
- #{attr_name}_will_change! if previous_time != time_with_zone
- @attributes_cache["#{attr_name}"] = time_with_zone
- end
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
- else
- super
+ private
+
+ def inherited(subclass)
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ TimeZoneConverter.new(type)
+ end
end
+ super
end
- private
- def create_time_zone_conversion_attribute?(name, column)
+ def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- (:datetime == column.type || :timestamp == column.type)
+ (:datetime == cast_type.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 c853fc0917..b3c8209a74 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -26,8 +26,6 @@ module ActiveRecord
protected
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 {
@@ -55,24 +53,12 @@ module ActiveRecord
# specified +value+. Empty strings for fixnum and float columns are
# turned into +nil+.
def write_attribute(attr_name, value)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes_cache.delete(attr_name)
- column = column_for_attribute(attr_name)
-
- # If we're dealing with a binary column, write the data to the cache
- # so we don't attempt to typecast multiple times.
- if column && column.binary?
- @attributes_cache[attr_name] = value
- end
+ write_attribute_with_type_cast(attr_name, value, true)
+ end
- if column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = type_cast_attribute_for_write(column, value)
- else
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
- end
+ def raw_write_attribute(attr_name, value)
+ write_attribute_with_type_cast(attr_name, value, false)
end
- alias_method :raw_write_attribute, :write_attribute
private
# Handle *= for method_missing.
@@ -80,10 +66,17 @@ module ActiveRecord
write_attribute(attribute_name, value)
end
- def type_cast_attribute_for_write(column, value)
- return value unless column
+ def write_attribute_with_type_cast(attr_name, value, should_type_cast)
+ attr_name = attr_name.to_s
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
+
+ if should_type_cast
+ @attributes.write_from_user(attr_name, value)
+ else
+ @attributes.write_from_database(attr_name, value)
+ end
- column.type_cast_for_write value
+ value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
new file mode 100644
index 0000000000..98ac63c7e1
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -0,0 +1,77 @@
+require 'active_record/attribute_set/builder'
+
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ delegate :keys, to: :initialized_attributes
+
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.transform_values(&:dup)
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ def ensure_initialized(key)
+ unless self[key].initialized?
+ write_from_database(key, nil)
+ end
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
new file mode 100644
index 0000000000..1e146a07da
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -0,0 +1,32 @@
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types
+
+ def initialize(types)
+ @types = types
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ attributes = build_attributes_from_values(values, additional_types)
+ add_uninitialized_attributes(attributes)
+ AttributeSet.new(attributes)
+ end
+
+ private
+
+ def build_attributes_from_values(values, additional_types)
+ values.each_with_object({}) do |(name, value), hash|
+ type = additional_types.fetch(name, types[name])
+ hash[name] = Attribute.from_database(name, value, type)
+ end
+ end
+
+ def add_uninitialized_attributes(attributes)
+ types.except(*attributes.keys).each do |name, type|
+ attributes[name] = Attribute.uninitialized(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
new file mode 100644
index 0000000000..890a1314d9
--- /dev/null
+++ b/activerecord/lib/active_record/attributes.rb
@@ -0,0 +1,122 @@
+module ActiveRecord
+ module Attributes # :nodoc:
+ extend ActiveSupport::Concern
+
+ Type = ActiveRecord::Type
+
+ included do
+ class_attribute :user_provided_columns, instance_accessor: false # :internal:
+ self.user_provided_columns = {}
+ end
+
+ module ClassMethods # :nodoc:
+ # Defines or overrides a attribute on this model. This allows customization of
+ # Active Record's type casting behavior, as well as adding support for user defined
+ # types.
+ #
+ # +name+ The name of the methods to define attribute methods for, and the column which
+ # this will persist to.
+ #
+ # +cast_type+ A type object that contains information about how to type cast the value.
+ # See the examples section for more information.
+ #
+ # ==== Options
+ # The options hash accepts the following options:
+ #
+ # +default+ is the default value that the column should use on a new record.
+ #
+ # ==== Examples
+ #
+ # The type detected by Active Record can be overridden.
+ #
+ # # db/schema.rb
+ # create_table :store_listings, force: true do |t|
+ # t.decimal :price_in_cents
+ # end
+ #
+ # # app/models/store_listing.rb
+ # class StoreListing < ActiveRecord::Base
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
+ #
+ # # before
+ # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, Type::Integer.new
+ # end
+ #
+ # # after
+ # store_listing.price_in_cents # => 10
+ #
+ # Users may also define their own custom types, as long as they respond to the methods
+ # defined on the value type. The `type_cast` method on your type object will be called
+ # with values both from the database, and from your controllers. See
+ # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your
+ # type objects inherit from an existing type, or the base value type.
+ #
+ # class MoneyType < ActiveRecord::Type::Integer
+ # def type_cast(value)
+ # if value.include?('$')
+ # price_in_dollars = value.gsub(/\$/, '').to_f
+ # price_in_dollars * 100
+ # else
+ # value.to_i
+ # end
+ # end
+ # end
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, MoneyType.new
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '$10.00')
+ # store_listing.price_in_cents # => 1000
+ def attribute(name, cast_type, options = {})
+ name = name.to_s
+ clear_caches_calculated_from_columns
+ # Assign a new hash to ensure that subclasses do not share a hash
+ self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type))
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
+ end
+
+ # Returns a hash of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ end
+
+ def reset_column_information # :nodoc:
+ super
+ clear_caches_calculated_from_columns
+ end
+
+ private
+
+ def add_user_provided_columns(schema_columns)
+ existing_columns = schema_columns.map do |column|
+ user_provided_columns[column.name] || column
+ end
+
+ existing_column_names = existing_columns.map(&:name)
+ new_columns = user_provided_columns.except(*existing_column_names).values
+
+ existing_columns + new_columns
+ end
+
+ def clear_caches_calculated_from_columns
+ @attributes_builder = nil
+ @column_names = nil
+ @column_types = nil
+ @columns = nil
+ @columns_hash = nil
+ @content_columns = nil
+ @default_attributes = nil
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 4f58d06f35..dd92e29199 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:
@@ -144,6 +147,7 @@ module ActiveRecord
private
def define_non_cyclic_method(name, &block)
+ return if method_defined?(name)
define_method(name) do |*args|
result = true; @_already_called ||= {}
# Loop prevention for validation of associations
@@ -176,30 +180,26 @@ module ActiveRecord
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
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
+ after_save save_method
+ elsif reflection.has_one?
+ define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
+ # 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
if reflection.validate? && !method_defined?(validation_method)
@@ -270,9 +270,11 @@ module ActiveRecord
# 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? }
+ self.class._reflections.values.any? do |reflection|
+ if reflection.options[:autosave]
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
end
@@ -301,7 +303,8 @@ module ActiveRecord
def association_valid?(reflection, record)
return true if record.destroyed? || record.marked_for_destruction?
- unless valid = record.valid?(self.validation_context)
+ 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}"
@@ -359,6 +362,7 @@ module ActiveRecord
raise ActiveRecord::Rollback unless saved
end
+ @new_record_before_save = false
end
# reconstruct the scope now that we know the owner's id
@@ -377,15 +381,16 @@ module ActiveRecord
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
+ elsif autosave != false
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+ if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
unless reflection.through_reflection
record[reflection.foreign_key] = key
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 9ec1feea97..f978fbd0a4 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -9,16 +9,19 @@ require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/class/subclasses'
require 'arel'
+require 'active_record/attribute_decorators'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
require 'active_record/relation/delegation'
+require 'active_record/attributes'
module ActiveRecord #:nodoc:
# = Active Record
@@ -138,6 +141,7 @@ module ActiveRecord #:nodoc:
#
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
# Query methods allow you to test whether an attribute value is present.
+ # For numeric values, present is defined as non-zero.
#
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
# to determine whether the user has a name:
@@ -217,25 +221,9 @@ module ActiveRecord #:nodoc:
#
# == Single table inheritance
#
- # Active Record allows inheritance by storing the name of the class in a column that by
- # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
- # This means that an inheritance looking like this:
- #
- # class Company < ActiveRecord::Base; end
- # class Firm < Company; end
- # class Client < Company; end
- # class PriorityClient < Client; end
- #
- # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
- # the companies table with type = "Firm". You can then fetch this row again using
- # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
- #
- # If you don't have a type column defined in your table, single-table inheritance won't
- # be triggered. In that case, it'll work just like normal subclasses with no special magic
- # for differentiating between them or reloading the right type with find.
- #
- # Note, all the attributes for all the cases are kept in the same table. Read more:
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ # Active Record allows inheritance by storing the name of the class in a
+ # column that is named "type" by default. See ActiveRecord::Inheritance for
+ # more details.
#
# == Connection to multiple databases in different models
#
@@ -296,7 +284,6 @@ module ActiveRecord #:nodoc:
include Core
include Persistence
- include NoTouching
include ReadonlyAttributes
include ModelSchema
include Inheritance
@@ -307,6 +294,8 @@ module ActiveRecord #:nodoc:
include Integration
include Validations
include CounterCache
+ include Attributes
+ include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
@@ -318,6 +307,7 @@ module ActiveRecord #:nodoc:
include NestedAttributes
include Aggregations
include Transactions
+ include NoTouching
include Reflection
include Serialization
include Store
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 35f19f0bc0..5955673b42 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -302,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/coders/json.rb b/activerecord/lib/active_record/coders/json.rb
new file mode 100644
index 0000000000..75d3bfe625
--- /dev/null
+++ b/activerecord/lib/active_record/coders/json.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module Coders # :nodoc:
+ class JSON # :nodoc:
+ def self.dump(obj)
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ def self.load(json)
+ ActiveSupport::JSON.decode(json) unless json.nil?
+ end
+ end
+ 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 759e162e19..a5fa9d6adc 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.
@@ -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
@@ -361,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 owner
@available.add conn
end
@@ -378,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.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_threadsafe?
+ 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
@@ -415,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(owner)
+ thread_id = owner.object_id
- @reserved_connections.delete thread_id if thread_id
+ @reserved_connections.delete thread_id
end
def new_connection
@@ -462,23 +462,44 @@ module ActiveRecord
#
# For example, suppose that you have 5 models, with the following hierarchy:
#
- # |
- # +-- Book
- # | |
- # | +-- ScaryBook
- # | +-- GoodBook
- # +-- Author
- # +-- BankAccount
+ # class Author < ActiveRecord::Base
+ # end
+ #
+ # class BankAccount < ActiveRecord::Base
+ # end
+ #
+ # class Book < ActiveRecord::Base
+ # establish_connection "library_db"
+ # end
+ #
+ # class ScaryBook < Book
+ # end
+ #
+ # class GoodBook < Book
+ # end
+ #
+ # And a database.yml that looked like this:
+ #
+ # development:
+ # database: my_application
+ # host: localhost
+ #
+ # library_db:
+ # database: library
+ # host: some.library.org
+ #
+ # Your primary database in the development environment is "my_application"
+ # but the Book model connects to a separate database called "library_db"
+ # (this can even be a database on a different machine).
#
- # Suppose that Book is to connect to a separate database (i.e. one other
- # than the default database). Then Book, ScaryBook and GoodBook will all use
- # the same connection pool. Likewise, Author and BankAccount will use the
- # same connection pool. However, the connection pool used by Author/BankAccount
- # is not the same as the one used by Book/ScaryBook/GoodBook.
+ # Book, ScaryBook and GoodBook will all use the same connection pool to
+ # "library_db" while Author, BankAccount, and any other models you create
+ # will use the default connection pool to "my_application".
#
- # Normally there is only a single ConnectionHandler instance, accessible via
- # ActiveRecord::Base.connection_handler. Active Record models use this to
- # determine the connection pool that they should use.
+ # The various connection pools are managed by a single instance of
+ # ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
+ # All Active Record models use this handler to determine the connection pool that they
+ # should use.
class ConnectionHandler
def initialize
# These caches are keyed by klass.name, NOT klass. Keying them by klass
@@ -538,7 +559,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
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 6eb59cc398..98e96099cb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -9,25 +9,26 @@ 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 ActiveRecord::Result instance.
- def select_all(arel, name = nil, binds = [])
- if arel.is_a?(Relation)
- relation = arel
- arel = relation.arel
- if !binds || binds.empty?
- binds = relation.bind_values
- end
+ # 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
@@ -47,10 +48,7 @@ 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)
- binds = []
- if arel.is_a?(Relation)
- arel, binds = arel.arel, arel.bind_values
- end
+ arel, binds = binds_from_relation arel, []
select_rows(to_sql(arel, binds), name, binds).map(&:first)
end
@@ -195,7 +193,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The mysql, mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for mysql versions below 5,
+ # isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
def transaction(options = {})
@@ -205,58 +203,30 @@ module ActiveRecord
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
-
yield
else
- within_new_transaction(options) { yield }
+ transaction_manager.within_new_transaction(options) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
- def within_new_transaction(options = {}) #:nodoc:
- transaction = begin_transaction(options)
- yield
- rescue Exception => error
- rollback_transaction if transaction
- raise
- ensure
- begin
- commit_transaction unless error
- rescue Exception
- rollback_transaction
- raise
- end
- end
+ attr_reader :transaction_manager #:nodoc:
- def current_transaction #:nodoc:
- @transaction
- end
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
def transaction_open?
- @transaction.open?
- end
-
- def begin_transaction(options = {}) #:nodoc:
- @transaction = @transaction.begin(options)
- end
-
- def commit_transaction #:nodoc:
- @transaction = @transaction.commit
- end
-
- def rollback_transaction #:nodoc:
- @transaction = @transaction.rollback
+ current_transaction.open?
end
def reset_transaction #:nodoc:
- @transaction = ClosedTransaction.new(self)
+ @transaction_manager = TransactionManager.new(self)
end
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
- @transaction.add_record(record)
+ current_transaction.add_record(record)
end
# Begins the transaction (and turns off auto-committing).
@@ -328,7 +298,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)
@@ -336,8 +306,8 @@ module ActiveRecord
end
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
- # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
- # an UPDATE statement, so in the mysql adapters we redefine this to do that.
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
key = update.key
subselect = subquery_for(key, select)
@@ -389,6 +359,13 @@ module ActiveRecord
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 adc23a6674..4a4506c7f5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -63,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
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 75501852ed..eb88845913 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -9,34 +9,11 @@ module ActiveRecord
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return "'#{quote_string(value)}'" unless column
-
- case column.type
- when :integer then value.to_i.to_s
- when :float then value.to_f.to_s
- else
- "'#{quote_string(value)}'"
- end
-
- when true, false
- if column && column.type == :integer
- value ? '1' : '0'
- else
- value ? quoted_true : quoted_false
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then "NULL"
- when BigDecimal then value.to_s('F')
- when Numeric, ActiveSupport::Duration then value.to_s
- when Date, Time then "'#{quoted_date(value)}'"
- when Symbol then "'#{quote_string(value.to_s)}'"
- when Class then "'#{value.to_s}'"
- else
- "'#{quote_string(YAML.dump(value))}'"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _quote(value)
end
# Cast a +value+ to a type that the database understands. For example,
@@ -47,34 +24,14 @@ module ActiveRecord
return value.id
end
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return value unless column
-
- case column.type
- when :integer then value.to_i
- when :float then value.to_f
- else
- value
- end
-
- when true, false
- if column && column.type == :integer
- value ? 1 : 0
- else
- value ? 't' : 'f'
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then nil
- when BigDecimal then value.to_s('F')
- when Numeric then value
- when Date, Time then quoted_date(value)
- when Symbol then value.to_s
- else
- to_type = column ? " to #{column.type}" : ""
- raise TypeError, "can't cast #{value.class}#{to_type}"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _type_cast(value)
+ rescue TypeError
+ to_type = column ? " to #{column.type}" : ""
+ raise TypeError, "can't cast #{value.class}#{to_type}"
end
# Quotes a string, escaping any ' (single quote) and \ (backslash)
@@ -99,7 +56,7 @@ module ActiveRecord
# This works for mysql and mysql2 where table.column can be used to
# resolve ambiguity.
#
- # We override this in the sqlite and postgresql adapters to use only
+ # We override this in the sqlite3 and postgresql adapters to use only
# the column name (as per syntax requirements).
def quote_table_name_for_assignment(table, attr)
quote_table_name("#{table}.#{attr}")
@@ -109,10 +66,18 @@ module ActiveRecord
"'t'"
end
+ def unquoted_true
+ 't'
+ end
+
def quoted_false
"'f'"
end
+ def unquoted_false
+ 'f'
+ end
+
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
@@ -124,6 +89,45 @@ module ActiveRecord
value.to_s(:db)
end
+
+ private
+
+ def types_which_need_no_typecasting
+ [nil, Numeric, String]
+ end
+
+ def _quote(value)
+ case value
+ when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ "'#{quote_string(value.to_s)}'"
+ when true then quoted_true
+ when false then quoted_false
+ when nil then "NULL"
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Numeric, ActiveSupport::Duration then value.to_s
+ when Date, Time then "'#{quoted_date(value)}'"
+ when Symbol then "'#{quote_string(value.to_s)}'"
+ when Class then "'#{value.to_s}'"
+ else
+ "'#{quote_string(YAML.dump(value))}'"
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ value.to_s
+ when true then unquoted_true
+ when false then unquoted_false
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Date, Time then quoted_date(value)
+ when *types_which_need_no_typecasting
+ value
+ else raise TypeError
+ end
+ 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
index a51691bfa8..adad6cd542 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -13,7 +13,7 @@ module ActiveRecord
end
def visit_AddColumn(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ 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
@@ -23,10 +23,12 @@ module ActiveRecord
def visit_AlterTable(o)
sql = "ALTER TABLE #{quote_table_name(o.name)} "
sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
end
def visit_ColumnDefinition(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ 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
@@ -41,6 +43,21 @@ module ActiveRecord
create_sql
end
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -64,7 +81,7 @@ module ActiveRecord
end
def add_column_options!(sql, options)
- sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(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"
@@ -75,9 +92,33 @@ module ActiveRecord
sql
end
+ def quote_value(value, column)
+ column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+ column.cast_type ||= type_for_column(column)
+
+ @conn.quote(value, column)
+ end
+
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
+
+ def type_for_column(column)
+ @conn.lookup_cast_type(column.sql_type)
+ 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 c39bf15e83..e44ccb7d81 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -15,7 +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:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -25,6 +25,37 @@ module ActiveRecord
class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
end
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
+ end
+
+ def primary_key
+ options[:primary_key] || default_primary_key
+ end
+
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
+ end
+
+ private
+ def default_primary_key
+ "id"
+ end
+ end
+
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
@@ -99,9 +130,11 @@ module ActiveRecord
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
+ # * <tt>:index</tt> -
+ # Create an index for the column. Can be either <tt>true</tt> or an options hash.
#
- # For clarity's sake: the precision is the number of significant digits,
- # while the scale is the number of digits that can be stored following
+ # Note: The precision is the total number of significant digits
+ # and the scale is the number of digits that can be stored following
# the decimal point. For example, the number 123.45 has a precision of 5
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
# range from -999.99 to 999.99.
@@ -123,17 +156,8 @@ module ActiveRecord
# Default is (38,0).
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
# Default unknown.
- # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
- # Default (9,0). Internal types NUMERIC and DECIMAL have different
- # storage rules, decimal being better.
- # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
- # NUMERIC is 19, and DECIMAL is 38.
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
- # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0).
- # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
#
# This method returns <tt>self</tt>.
#
@@ -172,18 +196,21 @@ module ActiveRecord
# What can be written like this with the regular calls to column:
#
# create_table :products do |t|
- # t.column :shop_id, :integer
- # t.column :creator_id, :integer
- # t.column :name, :string, default: "Untitled"
- # t.column :value, :string, default: "Untitled"
- # t.column :created_at, :datetime
- # t.column :updated_at, :datetime
+ # t.column :shop_id, :integer
+ # t.column :creator_id, :integer
+ # t.column :item_number, :string
+ # t.column :name, :string, default: "Untitled"
+ # t.column :value, :string, default: "Untitled"
+ # t.column :created_at, :datetime
+ # t.column :updated_at, :datetime
# end
+ # add_index :products, :item_number
#
# can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
+ # t.string :item_number, index: true
# t.string :name, :value, default: "Untitled"
# t.timestamps
# end
@@ -219,6 +246,8 @@ module ActiveRecord
raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
end
+ index_options = options.delete(:index)
+ index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
@columns_hash[name] = new_column_definition(name, type, options)
self
end
@@ -251,12 +280,22 @@ module ActiveRecord
column(:updated_at, :datetime, options)
end
+ # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
+ # by default, the <tt>:type</tt> option can be used to specify a different type.
+ #
+ # t.references(:user)
+ # t.references(:user, type: "string")
+ # t.belongs_to(:supplier, polymorphic: true)
+ #
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
index_options = options.delete(:index)
+ type = options.delete(:type) || :integer
args.each do |col|
- column("#{col}_id", :integer, options)
+ column("#{col}_id", type, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
@@ -264,6 +303,7 @@ module ActiveRecord
alias :belongs_to :references
def new_column_definition(name, type, options) # :nodoc:
+ type = aliased_types[type] || type
column = create_column_definition name, type
limit = options.fetch(:limit) do
native[type][:limit] if native[type].is_a?(Hash)
@@ -294,18 +334,36 @@ module ActiveRecord
def native
@native
end
+
+ def aliased_types
+ HashWithIndifferentAccess.new(
+ timestamp: :datetime,
+ )
+ end
end
class AlterTable # :nodoc:
attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
def initialize(td)
@td = td
@adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
end
def name; @td.name; end
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
def add_column(name, type, options)
name = name.to_s
type = type.to_sym
@@ -452,11 +510,14 @@ module ActiveRecord
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # <tt>references</tt> and <tt>belongs_to</tt> are acceptable.
+ # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
+ # by default, the <tt>:type</tt> option can be used to specify a different type.
#
# t.references(:user)
+ # t.references(:user, type: "string")
# t.belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
args.each do |ref_name|
@@ -471,6 +532,7 @@ module ActiveRecord
# t.remove_references(:user)
# t.remove_belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#remove_reference
def remove_references(*args)
options = args.extract_options!
args.each do |ref_name|
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index cdf0cbe218..9bd0401e40 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,5 +1,3 @@
-require 'ipaddr'
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
# The goal of this module is to move Adapter specific column
@@ -20,19 +18,13 @@ module ActiveRecord
def prepare_column_options(column, types)
spec = {}
spec[:name] = column.name.inspect
-
- # AR has an optimization which handles zero-scale decimals as integers. This
- # code ensures that the dumper still dumps the column as a decimal.
- spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
- 'decimal'
- else
- column.type.to_s
- end
- spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal'
+ spec[:type] = column.type.to_s
+ spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit]
spec[:precision] = column.precision.inspect if column.precision
spec[:scale] = column.scale.inspect if column.scale
spec[:null] = 'false' unless column.null
- spec[:default] = default_string(column.default) if column.has_default?
+ spec[:default] = schema_default(column) if column.has_default?
+ spec.delete(:default) if spec[:default].nil?
spec
end
@@ -43,28 +35,12 @@ module ActiveRecord
private
- def default_string(value)
- case value
- when BigDecimal
- value.to_s
- when Date, DateTime, Time
- "'#{value.to_s(:db)}'"
- when Range
- # infinity dumps as Infinity, which causes uninitialized constant error
- value.inspect.gsub('Infinity', '::Float::INFINITY')
- when IPAddr
- subnet_mask = value.instance_variable_get(:@mask_addr)
-
- # If the subnet mask is equal to /32, don't output it
- if subnet_mask == (2**32 - 1)
- "\"#{value.to_s}\""
- else
- "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\""
- end
- else
- value.inspect
- end
+ def schema_default(column)
+ default = column.type_cast_from_database(column.default)
+ unless default.nil?
+ column.type_cast_for_schema(default)
end
+ end
end
end
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 ad069f5e53..4957e1ac80 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -40,16 +40,17 @@ 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)
- index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names)
- if options[:unique]
- indexes(table_name).any?{ |i| i.unique && i.name == index_name }
- else
- indexes(table_name).any?{ |i| i.name == index_name }
- end
+ column_names = Array(column_name).map(&:to_s)
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names)
+ checks = []
+ checks << lambda { |i| i.name == index_name }
+ checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| i.unique } if options[:unique]
+
+ indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
# Returns an array of Column objects for the table specified by +table_name+.
@@ -71,7 +72,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]) &&
@@ -186,24 +188,23 @@ module ActiveRecord
def create_table(table_name, options = {})
td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
- if !options[:as]
- unless options[:id] == false
- pk = options.fetch(:primary_key) {
- Base.get_primary_key table_name.to_s.singularize
- }
-
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ if options[:id] != false && !options[:as]
+ pk = options.fetch(:primary_key) do
+ Base.get_primary_key table_name.to_s.singularize
end
- yield td if block_given?
+ td.primary_key pk, options.fetch(:id, :primary_key), options
end
+ yield td if block_given?
+
if options[:force] && table_exists?(table_name)
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
@@ -602,12 +603,18 @@ module ActiveRecord
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify
+ # a different type.
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
- # ====== Create a user_id column
+ # ====== Create a user_id integer column
#
# add_reference(:products, :user)
#
+ # ====== Create a user_id string column
+ #
+ # add_reference(:products, :user, type: :string)
+ #
# ====== Create a supplier_id and supplier_type columns
#
# add_belongs_to(:products, :supplier, polymorphic: true)
@@ -619,7 +626,8 @@ module ActiveRecord
def add_reference(table_name, ref_name, options = {})
polymorphic = options.delete(:polymorphic)
index_options = options.delete(:index)
- add_column(table_name, "#{ref_name}_id", :integer, options)
+ type = options.delete(:type) || :integer
+ add_column(table_name, "#{ref_name}_id", type, options)
add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
@@ -642,6 +650,115 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
+ end
+
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -740,6 +857,40 @@ module ActiveRecord
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]
@@ -754,7 +905,7 @@ module ActiveRecord
return option_strings
end
- # Overridden by the mysql adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
option_strings = Hash[column_names.map {|name| [name, '']}]
@@ -770,40 +921,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)
@@ -852,6 +969,12 @@ module ActiveRecord
def create_alter_table(name)
AlterTable.new create_table_definition(name, false, {})
end
+
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
+ end
+ 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 bc4884b538..4a7f2aaca8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -1,20 +1,7 @@
module ActiveRecord
module ConnectionAdapters
- class Transaction #:nodoc:
- attr_reader :connection
-
- def initialize(connection)
- @connection = connection
- @state = TransactionState.new
- end
-
- def state
- @state
- end
- end
-
class TransactionState
- attr_accessor :parent
+ attr_reader :parent
VALID_STATES = Set.new([:committed, :rolledback, nil])
@@ -43,82 +30,24 @@ module ActiveRecord
end
end
- class ClosedTransaction < Transaction #:nodoc:
- def number
- 0
- end
-
- def begin(options = {})
- RealTransaction.new(connection, self, options)
- end
-
- def closed?
- true
- end
-
- def open?
- false
- end
-
- def joinable?
- false
- end
-
- # This is a noop when there are no open transactions
- def add_record(record)
- end
+ class NullTransaction #:nodoc:
+ def initialize; end
+ def closed?; true; end
+ def open?; false; end
+ def joinable?; false; end
+ def add_record(record); end
end
- class OpenTransaction < Transaction #:nodoc:
- attr_reader :parent, :records
- attr_writer :joinable
-
- def initialize(connection, parent, options = {})
- super connection
-
- @parent = parent
- @records = []
- @finishing = false
- @joinable = options.fetch(:joinable, true)
- end
-
- # This state is necessary so that we correctly handle stuff that might
- # happen in a commit/rollback. But it's kinda distasteful. Maybe we can
- # find a better way to structure it in the future.
- def finishing?
- @finishing
- end
-
- def joinable?
- @joinable && !finishing?
- end
-
- def number
- if finishing?
- parent.number
- else
- parent.number + 1
- end
- end
-
- def begin(options = {})
- if finishing?
- parent.begin
- else
- SavepointTransaction.new(connection, self, options)
- end
- end
+ class Transaction #:nodoc:
- def rollback
- @finishing = true
- perform_rollback
- parent
- end
+ attr_reader :connection, :state, :records, :savepoint_name
+ attr_writer :joinable
- def commit
- @finishing = true
- perform_commit
- parent
+ def initialize(connection, options)
+ @connection = connection
+ @state = TransactionState.new
+ @records = []
+ @joinable = options.fetch(:joinable, true)
end
def add_record(record)
@@ -129,19 +58,25 @@ module ActiveRecord
end
end
- def rollback_records
+ def rollback
@state.set_state(:rolledback)
+ end
+
+ def rollback_records
records.uniq.each do |record|
begin
- record.rolledback!(parent.closed?)
+ record.rolledback! full_rollback?
rescue => e
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
end
- def commit_records
+ def commit
@state.set_state(:committed)
+ end
+
+ def commit_records
records.uniq.each do |record|
begin
record.committed!
@@ -151,19 +86,40 @@ module ActiveRecord
end
end
- def closed?
- false
+ def full_rollback?; true; end
+ def joinable?; @joinable; end
+ def closed?; false; end
+ def open?; !closed?; end
+ end
+
+ class SavepointTransaction < Transaction
+
+ def initialize(connection, savepoint_name, options)
+ super(connection, options)
+ if options[:isolation]
+ raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
+ end
+ connection.create_savepoint(@savepoint_name = savepoint_name)
+ end
+
+ def rollback
+ super
+ connection.rollback_to_savepoint(savepoint_name)
+ rollback_records
end
- def open?
- true
+ def commit
+ super
+ connection.release_savepoint(savepoint_name)
end
+
+ def full_rollback?; false; end
end
- class RealTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
- super
+ class RealTransaction < Transaction
+ def initialize(connection, options)
+ super
if options[:isolation]
connection.begin_isolated_db_transaction(options[:isolation])
else
@@ -171,37 +127,71 @@ module ActiveRecord
end
end
- def perform_rollback
+ def rollback
+ super
connection.rollback_db_transaction
rollback_records
end
- def perform_commit
+ def commit
+ super
connection.commit_db_transaction
commit_records
end
end
- class SavepointTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
- if options[:isolation]
- raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
- end
+ class TransactionManager #:nodoc:
+ def initialize(connection)
+ @stack = []
+ @connection = connection
+ end
- super
- connection.create_savepoint
+ def begin_transaction(options = {})
+ transaction =
+ if @stack.empty?
+ RealTransaction.new(@connection, options)
+ else
+ SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options)
+ end
+ @stack.push(transaction)
+ transaction
end
- def perform_rollback
- connection.rollback_to_savepoint
- rollback_records
+ def commit_transaction
+ @stack.pop.commit
end
- def perform_commit
- @state.set_state(:committed)
- @state.parent = parent.state
- connection.release_savepoint
+ def rollback_transaction
+ @stack.pop.rollback
+ end
+
+ def within_new_transaction(options = {})
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ transaction.rollback if transaction
+ raise
+ ensure
+ begin
+ transaction.commit unless error
+ rescue Exception
+ transaction.rollback
+ raise
+ ensure
+ @stack.pop if transaction
+ end
end
+
+ def open_transactions
+ @stack.size
+ end
+
+ def current_transaction
+ @stack.last || NULL_TRANSACTION
+ end
+
+ private
+ NULL_TRANSACTION = NullTransaction.new
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 11b28a4858..a1b6671664 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,17 +1,23 @@
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
+require 'active_record/type'
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:
extend ActiveSupport::Autoload
- autoload :Column
+ autoload_at 'active_record/connection_adapters/column' do
+ autoload :Column
+ autoload :NullColumn
+ end
autoload :ConnectionSpecification
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
@@ -39,7 +45,8 @@ module ActiveRecord
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
- autoload :ClosedTransaction
+ autoload :TransactionManager
+ autoload :NullTransaction
autoload :RealTransaction
autoload :SavepointTransaction
autoload :TransactionState
@@ -71,8 +78,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
@@ -90,13 +97,14 @@ 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
@schema_cache = SchemaCache.new self
@@ -104,6 +112,26 @@ module ActiveRecord
@prepared_statements = false
end
+ class BindCollector < Arel::Collectors::Bind
+ def compile(bvs, conn)
+ super(bvs.map { |bv| conn.quote(*bv.reverse) })
+ end
+ end
+
+ class SQLString < Arel::Collectors::SQLString
+ def compile(bvs, conn)
+ super(bvs)
+ end
+ end
+
+ def collector
+ if prepared_statements
+ SQLString.new
+ else
+ BindCollector.new
+ end
+ end
+
def valid_type?(type)
true
end
@@ -114,9 +142,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
@@ -127,19 +154,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_prepared_statements, @prepared_statements = @prepared_statements, false
- old_visitor, @visitor = @visitor, unprepared_visitor
yield
ensure
- @visitor, @prepared_statements = old_visitor, old_prepared_statements
+ @prepared_statements = old_prepared_statements
end
# Returns the human-readable name of the adapter. Use mixed case - one
@@ -148,28 +170,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
@@ -178,8 +191,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
@@ -187,7 +199,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
@@ -202,8 +213,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
@@ -213,12 +223,22 @@ 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
+ # Does this adapter support creating indexes in the same statement as
+ # creating the table?
+ def supports_indexes_in_create?
+ false
+ end
+
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -227,14 +247,12 @@ module ActiveRecord
def enable_extension(name)
end
- # A list of extensions, to be filled in by adapters that support them. At
- # the moment only postgresql does.
+ # 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
@@ -262,12 +280,6 @@ module ActiveRecord
def active?
end
- # Adapter should redefine this if it needs a threadsafe way to approximate
- # if the connection is active
- def active_threadsafe?
- active?
- end
-
# Disconnects from the database if already connected, and establishes a
# new connection with the database. Implementors should call super if they
# override the default implementation.
@@ -301,7 +313,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
@@ -323,10 +334,6 @@ module ActiveRecord
@connection
end
- def open_transactions
- @transaction.number
- end
-
def create_savepoint(name = nil)
end
@@ -336,16 +343,22 @@ module ActiveRecord
def release_savepoint(name = nil)
end
- def case_sensitive_modifier(node)
+ def case_sensitive_modifier(node, table_attribute)
node
end
+ 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)
table[attribute].lower.eq(table.lower(value))
end
def current_savepoint_name
- "active_record_#{open_transactions}"
+ current_transaction.savepoint_name
end
# Check the connection back in to the connection pool
@@ -353,8 +366,80 @@ module ActiveRecord
pool.checkin self
end
+ def type_map # :nodoc:
+ @type_map ||= Type::TypeMap.new.tap do |mapping|
+ initialize_type_map(mapping)
+ end
+ end
+
+ def new_column(name, default, cast_type, sql_type = nil, null = true)
+ Column.new(name, default, cast_type, sql_type, null)
+ end
+
+ def lookup_cast_type(sql_type) # :nodoc:
+ type_map.lookup(sql_type)
+ end
+
protected
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_limit m, %r(date)i, Type::Date
+ register_class_with_limit m, %r(time)i, Type::Time
+ register_class_with_limit m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
+
+ m.alias_type %r(blob)i, 'binary'
+ m.alias_type %r(clob)i, 'text'
+ m.alias_type %r(timestamp)i, 'datetime'
+ m.alias_type %r(numeric)i, 'decimal'
+ m.alias_type %r(number)i, 'decimal'
+ m.alias_type %r(double)i, 'float'
+
+ m.register_type(%r(decimal)i) do |sql_type|
+ scale = extract_scale(sql_type)
+ precision = extract_precision(sql_type)
+
+ if scale == 0
+ # FIXME: Remove this class as well
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ Type::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+ end
+
+ def reload_type_map # :nodoc:
+ type_map.clear
+ initialize_type_map(type_map)
+ end
+
+ def register_class_with_limit(mapping, key, klass) # :nodoc:
+ mapping.register_type(key) do |*args|
+ limit = extract_limit(args.last)
+ klass.new(limit: limit)
+ end
+ end
+
+ def extract_scale(sql_type) # :nodoc:
+ case sql_type
+ when /\((\d+)\)/ then 0
+ when /\((\d+)(,(\d+))\)/ then $3.to_i
+ end
+ end
+
+ def extract_precision(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
+ end
+
+ def extract_limit(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
def translate_exception_class(e, sql)
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
@@ -381,7 +466,13 @@ module ActiveRecord
end
def without_prepared_statement?(binds)
- !@prepared_statements || binds.empty?
+ !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
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 23edc8b955..e5417a9556 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -6,18 +6,35 @@ module ActiveRecord
include Savepoints
class SchemaCreation < AbstractAdapter::SchemaCreation
-
def visit_AddColumn(o)
add_column_position!(super, column_options(o))
end
private
+
+ def visit_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
+ 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
@@ -29,6 +46,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
@@ -38,29 +60,25 @@ module ActiveRecord
class Column < ConnectionAdapters::Column # :nodoc:
attr_reader :collation, :strict, :extra
- def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
+ def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
@strict = strict
@collation = collation
@extra = extra
- super(name, default, sql_type, null)
+ super(name, default, cast_type, sql_type, null)
+ assert_valid_default(default)
+ extract_default
end
- def extract_default(default)
+ def extract_default
if blob_or_text_column?
- if default.blank?
- null || strict ? nil : ''
- else
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- elsif missing_default_forged_as_empty_string?(default)
- nil
- else
- super
+ @default = null || strict ? nil : ''
+ elsif missing_default_forged_as_empty_string?(@default)
+ @default = nil
end
end
def has_default?
- return false if blob_or_text_column? #mysql forbids defaults on blob and text columns
+ return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns
super
end
@@ -68,54 +86,12 @@ module ActiveRecord
sql_type =~ /blob/i || type == :text
end
- # Must return the relevant concrete adapter
- def adapter
- raise NotImplementedError
- end
-
def case_sensitive?
collation && !collation.match(/_ci$/)
end
private
- def simplified_type(field_type)
- return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
-
- case field_type
- when /enum/i, /set/i then :string
- when /year/i then :integer
- when /bit/i then :binary
- else
- super
- end
- end
-
- def extract_limit(sql_type)
- case sql_type
- when /^enum\((.+)\)/i
- $1.split(',').map{|enum| enum.strip.length - 2}.max
- when /blob|text/i
- case sql_type
- when /tiny/i
- 255
- when /medium/i
- 16777215
- when /long/i
- 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
- else
- super # we could return 65535 here, but we leave it undecorated by default
- end
- when /^bigint/i; 8
- when /^int/i; 4
- when /^mediumint/i; 3
- when /^smallint/i; 2
- when /^tinyint/i; 1
- else
- super
- end
- end
-
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string) but we can for others (integer, datetime, boolean,
@@ -126,6 +102,12 @@ module ActiveRecord
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
+
+ def assert_valid_default(default)
+ if blob_or_text_column? && default.present?
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ end
end
##
@@ -155,7 +137,6 @@ module ActiveRecord
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
@@ -165,21 +146,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 })
@prepared_statements = true
- @visitor = Arel::Visitors::MySQL.new self
else
- @visitor = unprepared_visitor
+ @prepared_statements = false
end
end
@@ -206,17 +184,6 @@ 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:
#
@@ -225,6 +192,14 @@ module ActiveRecord
version[0] >= 5
end
+ def supports_indexes_in_create?
+ true
+ end
+
+ def supports_foreign_keys?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -241,12 +216,11 @@ module ActiveRecord
raise NotImplementedError
end
- # Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null, collation, extra = "") # :nodoc:
- Column.new(field, default, type, null, collation, extra)
+ def new_column(field, default, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
+ Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
end
- # Must return the Mysql error number from the exception, if the exception has an
+ # Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
@@ -254,12 +228,9 @@ module ActiveRecord
# QUOTING ==================================================
- def quote(value, column = nil)
- 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")
+ def _quote(value) # :nodoc:
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
else
super
end
@@ -277,10 +248,18 @@ module ActiveRecord
QUOTED_TRUE
end
+ def unquoted_true
+ 1
+ end
+
def quoted_false
QUOTED_FALSE
end
+ def unquoted_false
+ 0
+ end
+
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
@@ -296,6 +275,11 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def clear_cache!
+ super
+ reload_type_map
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
log(sql, name) { @connection.query(sql) }
@@ -405,7 +389,7 @@ module ActiveRecord
end
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -449,7 +433,9 @@ module ActiveRecord
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
field_name = set_field_encoding(field[:Field])
- new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
+ sql_type = field[:Type]
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -459,7 +445,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"
@@ -468,7 +454,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
@@ -487,7 +473,7 @@ module ActiveRecord
end
def rename_index(table_name, old_name, new_name)
- if (version[0] == 5 && version[1] >= 7) || version[0] >= 6
+ if supports_rename_index?
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
else
super
@@ -523,6 +509,34 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
case type.to_s
@@ -588,10 +602,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
@@ -614,6 +637,34 @@ module ActiveRecord
protected
+ def initialize_type_map(m) # :nodoc:
+ super
+ m.register_type(%r(enum)i) do |sql_type|
+ limit = sql_type[/^enum\((.+)\)/i, 1]
+ .split(',').map{|enum| enum.strip.length - 2}.max
+ Type::String.new(limit: limit)
+ end
+
+ m.register_type %r(tinytext)i, Type::Text.new(limit: 255)
+ m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255)
+ m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215)
+ m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215)
+ m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647)
+ m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647)
+ m.register_type %r(^bigint)i, Type::Integer.new(limit: 8)
+ m.register_type %r(^int)i, Type::Integer.new(limit: 4)
+ m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3)
+ m.register_type %r(^smallint)i, Type::Integer.new(limit: 2)
+ m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1)
+ m.register_type %r(^float)i, Type::Float.new(limit: 24)
+ m.register_type %r(^double)i, Type::Float.new(limit: 53)
+
+ m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
+ m.alias_type %r(set)i, 'varchar'
+ m.alias_type %r(year)i, 'integer'
+ m.alias_type %r(bit)i, 'binary'
+ end
+
# MySQL is too stupid to create a temporary table for use subquery, so we have
# to give it some prompting in the form of a subsubquery. Ugh!
def subquery_for(key, select)
@@ -683,15 +734,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
@@ -725,34 +774,39 @@ module ActiveRecord
private
+ def version
+ @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ end
+
+ def mariadb?
+ full_version =~ /mariadb/i
+ end
+
def supports_views?
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
+ def supports_rename_index?
+ mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
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'
+ unless variables.has_key?('sql_mode')
+ variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
@@ -773,6 +827,15 @@ module ActiveRecord
# ...and send them all in one query
@connection.query "SET #{encoding} #{variable_assignments}"
end
+
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index f2fbd5a8f2..5f9cc6edd0 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,101 +13,36 @@ 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, :default_function
- attr_accessor :primary, :coder
+ attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
- alias :encoded? :coder
+ delegate :type, :precision, :scale, :limit, :klass, :accessor,
+ :number?, :binary?, :changed?,
+ :type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
+ :type_cast_for_schema,
+ to: :cast_type
# Instantiates a new column in the table.
#
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
+ # +cast_type+ is the object used for type casting and type information.
# +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
# <tt>company_name varchar(60)</tt>.
# 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)
+ def initialize(name, default, cast_type, sql_type = nil, null = true)
@name = name
+ @cast_type = cast_type
@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 = default
@default_function = nil
- @primary = nil
- @coder = nil
- end
-
- # Returns +true+ if the column is either of type string or text.
- def text?
- type == :string || type == :text
- end
-
- # Returns +true+ if the column is either of type integer, float or decimal.
- def number?
- type == :integer || type == :float || type == :decimal
end
def has_default?
!default.nil?
end
- # Returns the Ruby class that corresponds to the abstract data type.
- def klass
- case type
- when :integer then Fixnum
- when :float then Float
- when :decimal then BigDecimal
- when :datetime, :timestamp, :time then Time
- when :date then Date
- when :text, :string, :binary then String
- when :boolean then Object
- end
- end
-
- def binary?
- type == :binary
- end
-
- # Casts a Ruby value to something appropriate for writing to the database.
- def type_cast_for_write(value)
- return value unless number?
-
- case value
- when FalseClass
- 0
- when TrueClass
- 1
- when String
- value.presence
- else
- value
- end
- end
-
- # Casts value (which is a String) to an appropriate instance.
- def type_cast(value)
- return nil if value.nil?
- return coder.load(value) if encoded?
-
- klass = self.class
-
- case type
- when :string, :text then value
- when :integer then klass.value_to_integer(value)
- when :float then value.to_f
- when :decimal then klass.value_to_decimal(value)
- when :datetime, :timestamp then klass.string_to_time(value)
- when :time then klass.string_to_dummy_time(value)
- when :date then klass.value_to_date(value)
- when :binary then klass.binary_to_string(value)
- when :boolean then klass.value_to_boolean(value)
- else value
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -116,177 +51,11 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def extract_default(default)
- type_cast(default)
- end
-
- class << self
- # Used to convert from BLOBs to Strings
- def binary_to_string(value)
- value
+ def with_type(type)
+ dup.tap do |clone|
+ clone.instance_variable_set('@cast_type', type)
end
-
- def value_to_date(value)
- if value.is_a?(String)
- return nil if value.empty?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def string_to_time(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- def string_to_dummy_time(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- dummy_time_string = "2000-01-01 #{string}"
-
- fast_string_to_time(dummy_time_string) || begin
- time_hash = Date._parse(dummy_time_string)
- return nil if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
-
- # convert something to a boolean
- def value_to_boolean(value)
- if value.is_a?(String) && value.empty?
- nil
- else
- TRUE_VALUES.include?(value)
- end
- end
-
- # Used to convert values to integer.
- # handle the case when an integer column is used to store boolean values
- def value_to_integer(value)
- case value
- when TrueClass, FalseClass
- value ? 1 : 0
- else
- value.to_i rescue nil
- end
- end
-
- # convert something to a BigDecimal
- def value_to_decimal(value)
- # Using .class is faster than .is_a? and
- # subclasses of BigDecimal will be handled
- # in the else clause
- if value.class == BigDecimal
- value
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- value.to_s.to_d
- end
- end
-
- protected
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- Date.new(year, mon, mday) rescue nil
- end
- end
-
- 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)
-
- 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)
- if string =~ Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def fallback_string_to_time(string)
- 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, :offset))
- end
end
-
- private
- def extract_limit(sql_type)
- $1.to_i if sql_type =~ /\((.*)\)/
- end
-
- def extract_precision(sql_type)
- $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
- end
-
- def extract_scale(sql_type)
- case sql_type
- when /^(numeric|decimal|number)\((\d+)\)/i then 0
- when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
- end
- end
-
- def simplified_type(field_type)
- case field_type
- when /int/i
- :integer
- when /float|double/i
- :float
- when /decimal|numeric|number/i
- extract_scale(field_type) == 0 ? :integer : :decimal
- when /datetime/i
- :datetime
- when /timestamp/i
- :timestamp
- when /time/i
- :time
- when /date/i
- :date
- when /clob/i, /text/i
- :text
- when /blob/i, /binary/i
- :binary
- when /char/i
- :string
- when /boolean/i
- :boolean
- end
- end
end
end
# :startdoc:
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 3f8b14bf67..d28a54b8f9 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -32,10 +32,15 @@ module ActiveRecord
# }
def initialize(url)
raise "Database URL cannot be empty" if url.blank?
- @uri = URI.parse(url)
- @adapter = @uri.scheme
+ @uri = uri_parser.parse(url)
+ @adapter = @uri.scheme.gsub('-', '_')
@adapter = "postgresql" if @adapter == "postgres"
- @query = @uri.query || ''
+
+ 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.
@@ -57,38 +62,46 @@ module ActiveRecord
# Converts the query parameters of the URI into a hash.
#
- # "localhost?pool=5&reap_frequency=2"
- # # => { "pool" => "5", "reap_frequency" => "2" }
+ # "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("=") }]
+ Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
end
def raw_config
- query_hash.merge({
- "adapter" => @adapter,
- "username" => uri.user,
- "password" => uri.password,
- "port" => uri.port,
- "database" => database,
- "host" => uri.host })
+ 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.hostname })
+ end
end
# Returns name of the database.
- # Sqlite3 expects this to be a full path or `:memory:`.
- def database
+ def database_from_path
if @adapter == 'sqlite3'
- if '/:memory:' == uri.path
- ':memory:'
- else
- uri.path
- end
+ # 'sqlite3:/foo' is absolute, because that makes sense. The
+ # corresponding relative version, 'sqlite3:foo', is handled
+ # elsewhere, as an "opaque".
+
+ uri.path
else
- uri.path.sub(%r{^/},"")
+ # 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
@@ -124,7 +137,7 @@ module ActiveRecord
if config
resolve_connection config
elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
- resolve_env_connection env.to_sym
+ resolve_symbol_connection env.to_sym
else
raise AdapterNotSpecified
end
@@ -147,7 +160,7 @@ module ActiveRecord
# config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
# spec = Resolver.new(config).spec(:production)
# spec.adapter_method
- # # => "sqlite3"
+ # # => "sqlite3_connection"
# spec.config
# # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
#
@@ -193,42 +206,41 @@ module ActiveRecord
#
def resolve_connection(spec)
case spec
- when Symbol, String
- resolve_env_connection spec
+ when Symbol
+ resolve_symbol_connection spec
+ when String
+ resolve_string_connection spec
when Hash
resolve_hash_connection spec
end
end
+ 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
+
# Takes the environment such as `:production` or `:development`.
# This requires that the @configurations was initialized with a key that
# matches.
#
- #
- # Resolver.new("production" => {}).resolve_env_connection(:production)
+ # Resolver.new("production" => {}).resolve_symbol_connection(:production)
# # => {}
#
- # Takes a connection URL.
- #
- # Resolver.new({}).resolve_env_connection("postgresql://localhost/foo")
- # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
- #
- def resolve_env_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_string_connection and
- # resolve_symbol_connection.
+ def resolve_symbol_connection(spec)
if config = configurations[spec.to_s]
- if spec.is_a?(String)
- 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"
- end
resolve_connection(config)
- elsif spec.is_a?(String)
- resolve_string_connection(spec)
else
- raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available configuration: #{configurations.inspect}")
+ raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
end
end
@@ -237,14 +249,19 @@ module ActiveRecord
# 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 url = spec.delete("url")
- connection_hash = resolve_string_connection(url)
+ if spec["url"] && spec["url"] !~ /^jdbc:/
+ connection_hash = resolve_url_connection(spec.delete("url"))
spec.merge!(connection_hash)
end
spec
end
- def resolve_string_connection(url)
+ # 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
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index b07b0cb826..39d52e6349 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -20,27 +20,20 @@ module ActiveRecord
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
rescue Mysql2::Error => error
if error.message.include?("Unknown database")
- raise ActiveRecord::NoDatabaseError.new(error.message)
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
else
- raise error
+ raise
end
end
end
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column # :nodoc:
- def adapter
- Mysql2Adapter
- end
- end
-
ADAPTER_NAME = 'Mysql2'
def initialize(connection, logger, connection_options, config)
super
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
configure_connection
end
@@ -69,10 +62,6 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation, extra = "") # :nodoc:
- Column.new(field, default, type, null, collation, strict_mode?, extra)
- end
-
def error_number(exception)
exception.error_number if exception.respond_to?(:error_number)
end
@@ -83,6 +72,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?
@@ -272,8 +269,8 @@ module ActiveRecord
super
end
- def version
- @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ def full_version
+ @full_version ||= @connection.info[:version]
end
def set_field_encoding field_name
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 49f0bfbcde..a03bc28744 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -36,9 +36,9 @@ module ActiveRecord
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
rescue Mysql::Error => error
if error.message.include?("Unknown database")
- raise ActiveRecord::NoDatabaseError.new(error.message)
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
else
- raise error
+ raise
end
end
end
@@ -66,35 +66,6 @@ module ActiveRecord
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
class MysqlAdapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column #:nodoc:
- def self.string_to_time(value)
- return super unless Mysql::Time === value
- new_time(
- value.year,
- value.month,
- value.day,
- value.hour,
- value.minute,
- value.second,
- value.second_part)
- end
-
- def self.string_to_dummy_time(v)
- return super unless Mysql::Time === v
- new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
- end
-
- def self.string_to_date(v)
- return super unless Mysql::Time === v
- new_date(v.year, v.month, v.day)
- end
-
- def adapter
- MysqlAdapter
- end
- end
-
ADAPTER_NAME = 'MySQL'
class StatementPool < ConnectionAdapters::StatementPool
@@ -156,10 +127,6 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation, extra = "") # :nodoc:
- Column.new(field, default, type, null, collation, strict_mode?, extra)
- end
-
def error_number(exception) # :nodoc:
exception.errno if exception.respond_to?(:errno)
end
@@ -222,6 +189,7 @@ module ActiveRecord
# Clears the prepared statements cache.
def clear_cache!
+ super
@statements.clear
end
@@ -294,126 +262,70 @@ module ActiveRecord
@connection.insert_id
end
- module Fields
- class Type
- def type; end
-
- def type_cast_for_write(value)
- value
- end
- end
-
- class Identity < Type
- def type_cast(value); value; end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_i rescue value ? 1 : 0
- end
- end
-
- class Date < Type
- def type; :date; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class DateTime < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_time value
- end
- end
-
- class Time < Type
- def type; :time; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
- end
- end
-
- class Float < Type
- def type; :float; end
-
- def type_cast(value)
- return if value.nil?
-
- value.to_f
+ module Fields # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ value.year,
+ value.month,
+ value.day,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
+ class Time < Type::Time # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ 2000,
+ 01,
+ 01,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
+ class << self
+ TYPES = Type::HashLookupTypeMap.new # :nodoc:
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- TYPES = {}
-
- # Register an MySQL +type_id+ with a typecasting object in
- # +type+.
- def self.register_type(type_id, type)
- TYPES[type_id] = type
- end
+ delegate :register_type, :alias_type, to: :TYPES
- def self.alias_type(new, old)
- TYPES[new] = TYPES[old]
- end
-
- def self.find_type(field)
- if field.type == Mysql::Field::TYPE_TINY && field.length > 1
- TYPES[Mysql::Field::TYPE_LONG]
- else
- TYPES.fetch(field.type) { Fields::Identity.new }
+ def find_type(field)
+ if field.type == Mysql::Field::TYPE_TINY && field.length > 1
+ TYPES.lookup(Mysql::Field::TYPE_LONG)
+ else
+ TYPES.lookup(field.type)
+ end
end
end
- register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
- register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
+ register_type Mysql::Field::TYPE_TINY, Type::Boolean.new
+ register_type Mysql::Field::TYPE_LONG, Type::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG
- register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new
- register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new
- register_type Mysql::Field::TYPE_DATE, Fields::Date.new
+ register_type Mysql::Field::TYPE_DATE, Type::Date.new
register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new
register_type Mysql::Field::TYPE_TIME, Fields::Time.new
- register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new
+ register_type Mysql::Field::TYPE_FLOAT, Type::Float.new
+ end
- Mysql::Field.constants.grep(/TYPE/).map { |class_name|
- Mysql::Field.const_get class_name
- }.reject { |const| TYPES.key? const }.each do |const|
- register_type const, Fields::Identity.new
- end
+ def initialize_type_map(m) # :nodoc:
+ super
+ m.register_type %r(datetime)i, Fields::DateTime.new
+ m.register_type %r(time)i, Fields::Time.new
end
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
@@ -431,7 +343,7 @@ module ActiveRecord
fields << field_name
if field.decimals > 0
- types[field_name] = Fields::Decimal.new
+ types[field_name] = Type::Decimal.new
else
types[field_name] = Fields.find_type field
end
@@ -447,7 +359,7 @@ module ActiveRecord
end
end
- def execute_and_free(sql, name = nil)
+ def execute_and_free(sql, name = nil) # :nodoc:
result = execute(sql, name)
ret = yield result
result.free
@@ -460,7 +372,7 @@ module ActiveRecord
end
alias :create :insert_sql
- def exec_delete(sql, name, binds)
+ def exec_delete(sql, name, binds) # :nodoc:
affected_rows = 0
exec_query(sql, name, binds) do |n|
@@ -497,7 +409,7 @@ module ActiveRecord
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
+ # place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
stmt.close
@@ -558,9 +470,9 @@ module ActiveRecord
rows
end
- # Returns the version of the connected MySQL server.
- def version
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ # Returns the full version of the connected MySQL server.
+ def full_version
+ @full_version ||= @connection.server_info
end
def set_field_encoding field_name
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 0b218f2bfd..1b74c039ce 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLColumn < Column
- module ArrayParser
+ module PostgreSQL
+ module ArrayParser # :nodoc:
DOUBLE_QUOTE = '"'
BACKSLASH = "\\"
@@ -9,35 +9,23 @@ module ActiveRecord
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
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- def parse_pg_array(string)
- parse_data(string)
+ def parse_pg_array(string) # :nodoc:
+ local_index = 0
+ array = []
+ while(local_index < string.length)
+ case string[local_index]
+ when BRACKET_OPEN
+ local_index,array = parse_array_contents(array, string, local_index + 1)
+ when BRACKET_CLOSE
+ return array
end
+ local_index += 1
end
- def parse_data(string)
- local_index = 0
- array = []
- while(local_index < string.length)
- case string[local_index]
- when BRACKET_OPEN
- local_index,array = parse_array_contents(array, string, local_index + 1)
- when BRACKET_CLOSE
- return array
- end
- local_index += 1
- end
+ array
+ end
- array
- end
+ private
def parse_array_contents(array, string, index)
is_escaping = false
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index 35ce881302..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,164 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- class PostgreSQLColumn < Column
- module Cast
- def point_to_string(point)
- "(#{point[0]},#{point[1]})"
- end
-
- def string_to_point(string)
- if string[0] == '(' && string[-1] == ')'
- string = string[1...-1]
- end
- string.split(',').map{ |v| Float(v) }
- end
-
- def string_to_time(string)
- return string unless String === string
-
- case string
- when 'infinity'; Float::INFINITY
- when '-infinity'; -Float::INFINITY
- when / BC$/
- super("-" + string.sub(/ BC$/, ""))
- else
- super
- end
- end
-
- def string_to_bit(value)
- case value
- when /^0x/i
- value[2..-1].hex.to_s(2) # Hexadecimal notation
- else
- value # Bit-string notation
- end
- end
-
- def hstore_to_string(object)
- if Hash === object
- object.map { |k,v|
- "#{escape_hstore(k)}=>#{escape_hstore(v)}"
- }.join ','
- else
- object
- end
- end
-
- def string_to_hstore(string)
- if string.nil?
- nil
- elsif String === string
- 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]
- }]
- else
- string
- end
- end
-
- def json_to_string(object)
- if Hash === object || Array === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def array_to_string(value, column, adapter)
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{casted_values.join(',')}}"
- end
-
- def range_to_string(object)
- from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin
- to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end
- "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}"
- end
-
- def string_to_json(string)
- if String === string
- ActiveSupport::JSON.decode(string)
- else
- string
- end
- end
-
- def string_to_cidr(string)
- if string.nil?
- nil
- elsif String === string
- begin
- IPAddr.new(string)
- rescue ArgumentError
- nil
- end
- else
- string
- end
- end
-
- def cidr_to_string(object)
- if IPAddr === object
- "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
- else
- object
- end
- end
-
- def string_to_array(string, oid)
- parse_pg_array(string).map {|val| type_cast_array(oid, val)}
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
-
- def quote_and_escape(value)
- case value
- when "NULL", Numeric
- value
- else
- "\"#{value.gsub(/"/,"\\\"")}\""
- 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
- 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..37e5c3859c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -0,0 +1,20 @@
+module ActiveRecord
+ module ConnectionAdapters
+ # PostgreSQL-specific extensions to column definitions in a table.
+ class PostgreSQLColumn < Column #:nodoc:
+ attr_accessor :array
+
+ def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
+ if sql_type =~ /\[\]$/
+ @array = true
+ super(name, default, cast_type, sql_type[0..sql_type.length - 3], null)
+ else
+ @array = false
+ super(name, default, cast_type, sql_type, null)
+ end
+
+ @default_function = default_function
+ 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 51ee2829b2..89a7257d77 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module DatabaseStatements
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
@@ -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, binds = [])
- exec_query(sql, name, binds).rows
+ execute_and_clear(sql, name, binds) do |result|
+ result.values
+ end
end
# Executes an INSERT query and returns the new record's ID
@@ -72,6 +94,11 @@ module ActiveRecord
super.insert
end
+ # The internal PostgreSQL identifier of the money data type.
+ MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
+ # The internal PostgreSQL identifier of the BYTEA data type.
+ BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
+
# create a 2D array representing the result set
def result_as_array(res) #:nodoc:
# check if we have any binary column and if they need escaping
@@ -134,31 +161,20 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
- exec_cache(sql, name, binds)
-
- types = {}
- fields = result.fields
- fields.each_with_index do |fname, i|
- ftype = result.ftype i
- fmod = result.fmod i
- types[fname] = type_map.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
+ execute_and_clear(sql, name, binds) do |result|
+ types = {}
+ fields = result.fields
+ fields.each_with_index do |fname, i|
+ ftype = result.ftype i
+ fmod = result.fmod i
+ types[fname] = get_oid_type(ftype, fmod, fname)
+ end
+ ActiveRecord::Result.new(fields, result.values, types)
end
-
- ret = ActiveRecord::Result.new(fields, result.values, types)
- result.clear
- return ret
end
def exec_delete(sql, name = 'SQL', binds = [])
- result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
- exec_cache(sql, name, binds)
- affected = result.cmd_tuples
- result.clear
- affected
+ execute_and_clear(sql, name, binds) {|result| result.cmd_tuples }
end
alias :exec_update :exec_delete
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index fae260a921..d28a2b4fa0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,380 +1,35 @@
-require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/postgresql/oid/infinity'
+
+require 'active_record/connection_adapters/postgresql/oid/array'
+require 'active_record/connection_adapters/postgresql/oid/bit'
+require 'active_record/connection_adapters/postgresql/oid/bit_varying'
+require 'active_record/connection_adapters/postgresql/oid/bytea'
+require 'active_record/connection_adapters/postgresql/oid/cidr'
+require 'active_record/connection_adapters/postgresql/oid/date'
+require 'active_record/connection_adapters/postgresql/oid/date_time'
+require 'active_record/connection_adapters/postgresql/oid/decimal'
+require 'active_record/connection_adapters/postgresql/oid/enum'
+require 'active_record/connection_adapters/postgresql/oid/float'
+require 'active_record/connection_adapters/postgresql/oid/hstore'
+require 'active_record/connection_adapters/postgresql/oid/inet'
+require 'active_record/connection_adapters/postgresql/oid/integer'
+require 'active_record/connection_adapters/postgresql/oid/json'
+require 'active_record/connection_adapters/postgresql/oid/jsonb'
+require 'active_record/connection_adapters/postgresql/oid/money'
+require 'active_record/connection_adapters/postgresql/oid/point'
+require 'active_record/connection_adapters/postgresql/oid/range'
+require 'active_record/connection_adapters/postgresql/oid/specialized_string'
+require 'active_record/connection_adapters/postgresql/oid/time'
+require 'active_record/connection_adapters/postgresql/oid/uuid'
+require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
+
+require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module OID
- class Type
- def type; end
- end
-
- class Identity < Type
- def type_cast(value)
- value
- end
- end
-
- class Bit < Type
- def type_cast(value)
- if String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_bit value
- else
- value
- end
- end
- end
-
- class Bytea < Type
- def type_cast(value)
- return if value.nil?
- PGconn.unescape_bytea value
- end
- end
-
- class Money < Type
- 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.]/, '')
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
- value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
- end
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Vector < Type
- attr_reader :delim, :subtype
-
- # +delim+ corresponds to the `typdelim` column in the pg_types
- # table. +subtype+ is derived from the `typelem` column in the
- # pg_types table.
- def initialize(delim, subtype)
- @delim = delim
- @subtype = subtype
- end
-
- # FIXME: this should probably split on +delim+ and use +subtype+
- # to cast the values. Unfortunately, the current Rails behavior
- # is to just return the string.
- def type_cast(value)
- value
- end
- end
-
- class Point < Type
- def type_cast(value)
- if String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_point value
- else
- value
- end
- end
- end
-
- class Array < Type
- attr_reader :subtype
- def initialize(subtype)
- @subtype = subtype
- end
-
- def type_cast(value)
- if String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
- else
- value
- end
- end
- end
-
- class Range < Type
- attr_reader :subtype
- def initialize(subtype)
- @subtype = subtype
- end
-
- 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,
- 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
- end
-
- def type_cast(value)
- return if value.nil? || value == 'empty'
- 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
- end
-
- ::Range.new(from, to, extracted[:exclude_end])
- end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_integer value
- end
- end
-
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- class Timestamp < Type
- def type; :timestamp; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::PostgreSQLColumn.string_to_time value
- end
- end
-
- class Date < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class Time < Type
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
- end
- end
-
- class Float < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_f
- end
- end
-
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Hstore < Type
- 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_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
- end
- end
-
- class Json < Type
- 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 TypeMap
- def initialize
- @mapping = {}
- end
-
- def []=(oid, type)
- @mapping[oid] = type
- end
-
- def [](oid)
- @mapping[oid]
- end
-
- def clear
- @mapping.clear
- end
-
- def key?(oid)
- @mapping.key? oid
- end
-
- def fetch(ftype, fmod)
- # The type for the numeric depends on the width of the field,
- # so we'll do something special here.
- #
- # When dealing with decimal columns:
- #
- # places after decimal = fmod - 4 & 0xffff
- # places before decimal = (fmod - 4) >> 16 & 0xffff
- if ftype == 1700 && (fmod - 4 & 0xffff).zero?
- ftype = 23
- end
-
- @mapping.fetch(ftype) { |oid| yield oid, fmod }
- end
- end
-
- # 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
- # objects as values.
- NAMES = Hash.new { |h,k| # :nodoc:
- h[k] = OID::Identity.new
- }
-
- # 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
- end
-
- # Alias the +old+ type to the +new+ type.
- def self.alias_type(new, old)
- NAMES[new] = NAMES[old]
- end
-
- # Is +name+ a registered type?
- def self.registered_type?(name)
- NAMES.key? name
- 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'
-
- 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 'timestamp', OID::Timestamp.new
- register_type 'timestamptz', OID::Timestamp.new
- register_type 'date', OID::Date.new
- register_type 'time', OID::Time.new
-
- register_type 'path', OID::Identity.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'
+ module PostgreSQL
+ module OID # :nodoc:
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
new file mode 100644
index 0000000000..cd5efe2bb8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -0,0 +1,96 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Array < Type::Value # :nodoc:
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
+ delegate :type, to: :subtype
+
+ def initialize(subtype, delimiter = ',')
+ @subtype = subtype
+ @delimiter = delimiter
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ type_cast_array(parse_pg_array(value), :type_cast_from_database)
+ else
+ super
+ end
+ end
+
+ def type_cast_from_user(value)
+ type_cast_array(value, :type_cast_from_user)
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
+ end
+
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
+ else
+ quote_and_escape(subtype.type_cast_for_database(value))
+ end
+ end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
new file mode 100644
index 0000000000..1dbb40ca1d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bit < Type::Value # :nodoc:
+ def type
+ :bit
+ end
+
+ def type_cast(value)
+ if ::String === value
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
new file mode 100644
index 0000000000..4c21097d48
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class BitVarying < OID::Bit # :nodoc:
+ def type
+ :bit_varying
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
new file mode 100644
index 0000000000..997613d7be
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -0,0 +1,14 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bytea < Type::Binary # :nodoc:
+ def type_cast_from_database(value)
+ return if value.nil?
+ PGconn.unescape_bytea(super)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
new file mode 100644
index 0000000000..a53b4ee8e2
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Cidr < Type::Value # :nodoc:
+ def type
+ :cidr
+ end
+
+ def type_cast_for_schema(value)
+ subnet_mask = value.instance_variable_get(:@mask_addr)
+
+ # If the subnet mask is equal to /32, don't output it
+ if subnet_mask == (2**32 - 1)
+ "\"#{value.to_s}\""
+ else
+ "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\""
+ end
+ end
+
+ def type_cast_for_database(value)
+ if IPAddr === value
+ "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ else
+ value
+ end
+ end
+
+ def cast_value(value)
+ if value.nil?
+ nil
+ elsif String === value
+ begin
+ IPAddr.new(value)
+ rescue ArgumentError
+ nil
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..1d8d264530
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
new file mode 100644
index 0000000000..b9e7894e5c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ case value
+ when 'infinity' then ::Float::INFINITY
+ when '-infinity' then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
new file mode 100644
index 0000000000..43d22c8daf
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Decimal < Type::Decimal # :nodoc:
+ def infinity(options = {})
+ BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
new file mode 100644
index 0000000000..77d5038efd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Enum < Type::Value # :nodoc:
+ def type
+ :enum
+ end
+
+ def type_cast(value)
+ value.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
new file mode 100644
index 0000000000..78ef94b912
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Float < Type::Float # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ case value
+ when ::Float then value
+ when 'Infinity' then ::Float::INFINITY
+ when '-Infinity' then -::Float::INFINITY
+ when 'NaN' then ::Float::NAN
+ else value.to_f
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
new file mode 100644
index 0000000000..be4525c94f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Hstore < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :hstore
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::Hash[value.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]
+ }]
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
new file mode 100644
index 0000000000..96486fa65b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Inet < Cidr # :nodoc:
+ def type
+ :inet
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
new file mode 100644
index 0000000000..e47780399a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ module Infinity # :nodoc:
+ def infinity(options = {})
+ options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
new file mode 100644
index 0000000000..59abdc0009
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Integer < Type::Integer # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
new file mode 100644
index 0000000000..e12ddd9901
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -0,0 +1,35 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Json < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :json
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
new file mode 100644
index 0000000000..34ed32ad35
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Jsonb < Json # :nodoc:
+ def type
+ :jsonb
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ # Postgres does not preserve insignificant whitespaces when
+ # roundtripping jsonb columns. This causes some false positives for
+ # the comparison here. Therefore, we need to parse and re-dump the
+ # raw value here to ensure the insignificant whitespaces are
+ # consitent with our encoder's output.
+ raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
+ super(raw_old_value, new_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
new file mode 100644
index 0000000000..df890c2ed6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Money < Type::Decimal # :nodoc:
+ include Infinity
+
+ class_attribute :precision
+
+ def type
+ :money
+ end
+
+ def scale
+ 2
+ end
+
+ def cast_value(value)
+ 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.]/, '')
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ end
+
+ super(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
new file mode 100644
index 0000000000..bac8b01d6b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Point < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :point
+ end
+
+ def type_cast(value)
+ case value
+ when ::String
+ if value[0] == '(' && value[-1] == ')'
+ value = value[1...-1]
+ end
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
new file mode 100644
index 0000000000..ae967d5167
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -0,0 +1,76 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Range < Type::Value # :nodoc:
+ attr_reader :subtype, :type
+
+ def initialize(subtype, type)
+ @subtype = subtype
+ @type = type
+ end
+
+ def type_cast_for_schema(value)
+ value.inspect.gsub('Infinity', '::Float::INFINITY')
+ end
+
+ def cast_value(value)
+ return if value == 'empty'
+ return value if value.is_a?(::Range)
+
+ extracted = extract_bounds(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
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ 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?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
new file mode 100644
index 0000000000..2d2fede4e8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class SpecializedString < Type::String # :nodoc:
+ attr_reader :type
+
+ def initialize(type)
+ @type = type
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
new file mode 100644
index 0000000000..8f0246eddb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Time < Type::Time # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
new file mode 100644
index 0000000000..e396ff4a1e
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -0,0 +1,85 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ # This class uses the data from PostgreSQL pg_type table to build
+ # the OID -> Type mapping.
+ # - OID is and integer representing the type.
+ # - Type is an OID::Type object.
+ # This class has side effects on the +store+ passed during initialization.
+ class TypeMapInitializer # :nodoc:
+ def initialize(store)
+ @store = store
+ end
+
+ def run(records)
+ nodes = records.reject { |row| @store.key? row['oid'].to_i }
+ mapped, nodes = nodes.partition { |row| @store.key? row['typname'] }
+ ranges, nodes = nodes.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' }
+ composites, nodes = nodes.partition { |row| row['typelem'] != '0' }
+
+ mapped.each { |row| register_mapped_type(row) }
+ enums.each { |row| register_enum_type(row) }
+ domains.each { |row| register_domain_type(row) }
+ arrays.each { |row| register_array_type(row) }
+ ranges.each { |row| register_range_type(row) }
+ composites.each { |row| register_composite_type(row) }
+ end
+
+ private
+ def register_mapped_type(row)
+ alias_type row['oid'], row['typname']
+ end
+
+ def register_enum_type(row)
+ register row['oid'], OID::Enum.new
+ end
+
+ def register_array_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Array.new(subtype, row['typdelim'])
+ end
+ end
+
+ def register_range_type(row)
+ if subtype = @store.lookup(row['rngsubtype'].to_i)
+ register row['oid'], OID::Range.new(subtype, row['typname'].to_sym)
+ end
+ end
+
+ def register_domain_type(row)
+ if base_type = @store.lookup(row["typbasetype"].to_i)
+ register row['oid'], base_type
+ else
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ end
+ end
+
+ def register_composite_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Vector.new(row['typdelim'], subtype)
+ end
+ end
+
+ def register(oid, oid_type)
+ oid = assert_valid_registration(oid, oid_type)
+ @store.register_type(oid, oid_type)
+ end
+
+ def alias_type(oid, target)
+ oid = assert_valid_registration(oid, target)
+ @store.alias_type(oid, target)
+ end
+
+ def assert_valid_registration(oid, oid_type)
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
+ oid.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
new file mode 100644
index 0000000000..dd97393eac
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Uuid < Type::Value # :nodoc:
+ RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [1-5][a-fA-F0-9]{3}-?
+ [8-Bab][a-fA-F0-9]{3}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?\}?\z}x
+
+ def type
+ :uuid
+ end
+
+ def type_cast(value)
+ value.to_s[RFC_4122, 0]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
new file mode 100644
index 0000000000..de4187b028
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Vector < Type::Value # :nodoc:
+ attr_reader :delim, :subtype
+
+ # +delim+ corresponds to the `typdelim` column in the pg_types
+ # table. +subtype+ is derived from the `typelem` column in the
+ # pg_types table.
+ def initialize(delim, subtype)
+ @delim = delim
+ @subtype = subtype
+ end
+
+ # FIXME: this should probably split on +delim+ and use +subtype+
+ # to cast the values. Unfortunately, the current Rails behavior
+ # is to just return the string.
+ def type_cast(value)
+ value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..334af7c598
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,28 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ 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 c1f978a081..cf5c8d288e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -1,141 +1,35 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
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.
def quote(value, column = nil) #:nodoc:
return super unless column
- sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale)
-
case value
- when Range
- if /range$/ =~ sql_type
- "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}"
- else
- super
- end
- when Array
- case sql_type
- when 'point' then super(PostgreSQLColumn.point_to_string(value))
- when 'json' then super(PostgreSQLColumn.json_to_string(value))
- else
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'"
- else
- super
- end
- end
- when Hash
- case sql_type
- when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column)
- when 'json' then super(PostgreSQLColumn.json_to_string(value), column)
- else super
- end
- when IPAddr
- case sql_type
- when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
- else super
- end
when Float
- if value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- elsif value.infinite? || value.nan?
+ if value.infinite? || value.nan?
"'#{value.to_s}'"
else
super
end
- when Numeric
- if sql_type == 'money' || [:string, :text].include?(column.type)
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
- else
- super
- end
- when String
- case sql_type
- when 'bytea' then "'#{escape_bytea(value)}'"
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
else
super
end
end
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when Range
- if /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
- else
- super(value, column)
- end
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- case column.sql_type
- when 'point' then PostgreSQLColumn.point_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else
- if column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- else
- super(value, column)
- end
- end
- when String
- 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 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- when IPAddr
- if %w(inet cidr).include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
- else
- super(value, column)
- end
- else
- super(value, column)
- end
- end
-
# Quotes strings for use in SQL input.
def quote_string(s) #:nodoc:
@connection.escape(s)
@@ -150,14 +44,7 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name)
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
-
- unless name_part
- quote_column_name(schema)
- else
- table_name, name_part = extract_pg_identifier_from_name(name_part)
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
- end
+ Utils.extract_schema_qualified_name(name.to_s).quoted
end
def quote_table_name_for_assignment(table, attr)
@@ -177,11 +64,54 @@ module ActiveRecord
result = "#{result}.#{sprintf("%06d", value.usec)}"
end
- if value.year < 0
- result = result.sub(/^-/, "") + " BC"
+ if value.year <= 0
+ bce_year = format("%04d", -value.year + 1)
+ result = result.sub(/^-?\d+/, bce_year) + " BC"
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, column)
+ end
+ end
+
+ private
+
+ def _quote(value)
+ case value
+ when Type::Binary::Data
+ "'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Type::Binary::Data
+ # 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.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
+ else
+ super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index bc775394a6..52b307c432 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -1,12 +1,12 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module ReferentialIntegrity
- def supports_disable_referential_integrity? #:nodoc:
+ module PostgreSQL
+ module ReferentialIntegrity # :nodoc:
+ def supports_disable_referential_integrity? # :nodoc:
true
end
- def disable_referential_integrity #:nodoc:
+ def disable_referential_integrity # :nodoc:
if supports_disable_referential_integrity?
begin
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
new file mode 100644
index 0000000000..83554bbf74
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -0,0 +1,154 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module ColumnMethods
+ def xml(*args)
+ options = args.extract_options!
+ column(args[0], :xml, options)
+ end
+
+ def tsvector(*args)
+ options = args.extract_options!
+ column(args[0], :tsvector, options)
+ end
+
+ def int4range(name, options = {})
+ column(name, :int4range, options)
+ end
+
+ def int8range(name, options = {})
+ column(name, :int8range, options)
+ end
+
+ def tsrange(name, options = {})
+ column(name, :tsrange, options)
+ end
+
+ def tstzrange(name, options = {})
+ column(name, :tstzrange, options)
+ end
+
+ def numrange(name, options = {})
+ column(name, :numrange, options)
+ end
+
+ def daterange(name, options = {})
+ column(name, :daterange, options)
+ end
+
+ def hstore(name, options = {})
+ column(name, :hstore, options)
+ end
+
+ def ltree(name, options = {})
+ column(name, :ltree, options)
+ end
+
+ def inet(name, options = {})
+ column(name, :inet, options)
+ end
+
+ def cidr(name, options = {})
+ column(name, :cidr, options)
+ end
+
+ def macaddr(name, options = {})
+ column(name, :macaddr, options)
+ end
+
+ def uuid(name, options = {})
+ column(name, :uuid, options)
+ end
+
+ def json(name, options = {})
+ column(name, :json, options)
+ end
+
+ def jsonb(name, options = {})
+ column(name, :jsonb, options)
+ end
+
+ def citext(name, options = {})
+ column(name, :citext, options)
+ end
+
+ def point(name, options = {})
+ column(name, :point, options)
+ end
+
+ def bit(name, options)
+ column(name, :bit, options)
+ end
+
+ def bit_varying(name, options)
+ column(name, :bit_varying, options)
+ end
+
+ def money(name, options)
+ column(name, :money, options)
+ end
+ end
+
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
+ attr_accessor :array
+ end
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # a record (as primary keys cannot be +nil+). This might be done via the
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
+ def primary_key(name, type = :primary_key, options = {})
+ return super unless type == :uuid
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()')
+ options[:primary_key] = true
+ column name, type, options
+ end
+
+ def column(name, type = nil, options = {})
+ super
+ column = self[name]
+ column.array = options[:array]
+
+ self
+ end
+
+ private
+
+ def create_column_definition(name, type)
+ PostgreSQL::ColumnDefinition.new name, type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ 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 ae8ede4b42..7042817672 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,18 +1,18 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
class SchemaCreation < AbstractAdapter::SchemaCreation
private
def visit_AddColumn(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}"
add_column_options!(sql, column_options(o))
end
def visit_ColumnDefinition(o)
sql = super
- if o.primary_key? && o.type == :uuid
+ if o.primary_key? && o.type != :primary_key
sql << " PRIMARY KEY "
add_column_options!(sql, column_options(o))
end
@@ -31,10 +31,14 @@ module ActiveRecord
super
end
end
- end
- def schema_creation
- SchemaCreation.new self
+ def type_for_column(column)
+ if column.array
+ @conn.lookup_cast_type("#{column.sql_type}[]")
+ else
+ super
+ end
+ end
end
module SchemaStatements
@@ -101,19 +105,16 @@ module ActiveRecord
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- return false unless table
-
- binds = [[nil, table]]
- binds << [nil, schema] if schema
+ name = Utils.extract_schema_qualified_name(name.to_s)
+ return false unless name.identifier
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')
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
+ WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
@@ -185,13 +186,17 @@ 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 = type_map.fetch(oid.to_i, fmod.to_i) {
- OID::Identity.new
- }
- PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
+ oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
+ default_value = extract_value_from_default(oid, default)
+ default_function = extract_default_function(default_value, default)
+ new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
end
end
+ def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
+ end
+
# Returns the current database name.
def current_database
query('select current_database()', 'SCHEMA')[0][0]
@@ -276,9 +281,9 @@ module ActiveRecord
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
- result.split('.').last
+ Utils.extract_schema_qualified_name(result)
rescue ActiveRecord::StatementInvalid
- "#{table_name}_#{pk || 'id'}_seq"
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq")
end
def serial_sequence(table, column)
@@ -315,24 +320,27 @@ module ActiveRecord
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname, seq.relname
+ SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_constraint cons
+ pg_constraint cons,
+ pg_namespace nsp
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
+ AND seq.relnamespace = nsp.oid
AND cons.contype = 'p'
+ AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
if result.nil? or result.empty?
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname,
+ SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
@@ -344,13 +352,19 @@ module ActiveRecord
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
- [result.first, result.last]
+ pk = result.shift
+ if result.last
+ [pk, PostgreSQL::Name.new(*result)]
+ else
+ [pk, nil]
+ end
rescue
nil
end
@@ -369,8 +383,8 @@ module ActiveRecord
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
@@ -378,7 +392,7 @@ module ActiveRecord
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
pk, seq = pk_and_sequence_for(new_name)
- if seq == "#{table_name}_#{pk}_seq"
+ if seq && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
end
@@ -408,13 +422,24 @@ 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)
+ return unless column
+
+ alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ execute alter_column_query % "DROP DEFAULT"
+ else
+ execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
+ end
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
@@ -439,6 +464,42 @@ module ActiveRecord
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
@@ -488,7 +549,7 @@ module ActiveRecord
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
- s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
+ s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
[super, *order_columns].join(', ')
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
new file mode 100644
index 0000000000..0290bcb48c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ # Value Object to hold a schema qualified name.
+ # This is usually the name of a PostgreSQL relation but it can also represent
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
+ # double quoting.
+ class Name # :nodoc:
+ SEPARATOR = "."
+ attr_reader :schema, :identifier
+
+ def initialize(schema, identifier)
+ @schema, @identifier = unquote(schema), unquote(identifier)
+ end
+
+ def to_s
+ parts.join SEPARATOR
+ end
+
+ def quoted
+ parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR
+ end
+
+ def ==(o)
+ o.class == self.class && o.parts == parts
+ end
+ alias_method :eql?, :==
+
+ def hash
+ parts.hash
+ end
+
+ protected
+ def unquote(part)
+ return unless part
+ part.gsub(/(^"|"$)/,'')
+ end
+
+ def parts
+ @parts ||= [@schema, @identifier].compact
+ end
+ end
+
+ module Utils # :nodoc:
+ extend self
+
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
+ # extracted from +string+.
+ # +schema+ is nil if not specified in +string+.
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
+ #
+ # * <tt>table_name</tt>
+ # * <tt>"table.name"</tt>
+ # * <tt>schema_name.table_name</tt>
+ # * <tt>schema_name."table.name"</tt>
+ # * <tt>"schema_name".table_name</tt>
+ # * <tt>"schema.name"."table name"</tt>
+ def extract_schema_qualified_name(string)
+ table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse
+ PostgreSQL::Name.new(schema, table)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 36c7462419..eede374678 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,12 +1,15 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
+
+require 'active_record/connection_adapters/postgresql/utils'
+require 'active_record/connection_adapters/postgresql/column'
require 'active_record/connection_adapters/postgresql/oid'
-require 'active_record/connection_adapters/postgresql/cast'
-require 'active_record/connection_adapters/postgresql/array_parser'
require 'active_record/connection_adapters/postgresql/quoting'
+require 'active_record/connection_adapters/postgresql/referential_integrity'
+require 'active_record/connection_adapters/postgresql/schema_definitions'
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 'arel/visitors/bind_visitor'
# Make sure we're using pg high enough for PGResult#values
@@ -43,222 +46,6 @@ module ActiveRecord
end
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)
- 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:
@@ -287,142 +74,16 @@ module ActiveRecord
# In addition, default connection parameters of libpq can be set per environment variables.
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :array
- end
-
- module ColumnMethods
- def xml(*args)
- options = args.extract_options!
- column(args[0], 'xml', options)
- end
-
- def tsvector(*args)
- options = args.extract_options!
- column(args[0], 'tsvector', options)
- end
-
- def int4range(name, options = {})
- column(name, 'int4range', options)
- end
-
- def int8range(name, options = {})
- column(name, 'int8range', options)
- end
-
- def tsrange(name, options = {})
- column(name, 'tsrange', options)
- end
-
- def tstzrange(name, options = {})
- column(name, 'tstzrange', options)
- end
-
- def numrange(name, options = {})
- column(name, 'numrange', options)
- end
-
- def daterange(name, options = {})
- column(name, 'daterange', options)
- end
-
- def hstore(name, options = {})
- column(name, 'hstore', options)
- end
-
- def ltree(name, options = {})
- column(name, 'ltree', options)
- end
-
- def inet(name, options = {})
- column(name, 'inet', options)
- end
-
- def cidr(name, options = {})
- column(name, 'cidr', options)
- end
-
- def macaddr(name, options = {})
- column(name, 'macaddr', options)
- end
-
- def uuid(name, options = {})
- column(name, 'uuid', options)
- end
-
- def json(name, options = {})
- column(name, 'json', options)
- end
- end
-
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- include ColumnMethods
-
- # Defines the primary key field.
- # Use of the native PostgreSQL UUID type is supported, and can be used
- # by defining your tables as such:
- #
- # create_table :stuffs, id: :uuid do |t|
- # t.string :content
- # t.timestamps
- # end
- #
- # By default, this will use the +uuid_generate_v4()+ function from the
- # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
- # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
- # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
- # set the +:default+ option to +nil+:
- #
- # create_table :stuffs, id: false do |t|
- # t.primary_key :id, :uuid, default: nil
- # t.uuid :foo_id
- # t.timestamps
- # end
- #
- # You may also pass a different UUID generation function from +uuid-ossp+
- # or another library.
- #
- # Note that setting the UUID primary key default value to +nil+ will
- # require you to assure that you always provide a UUID value before saving
- # a record (as primary keys cannot be +nil+). This might be done via the
- # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
- def primary_key(name, type = :primary_key, options = {})
- return super unless type == :uuid
- options[:default] = options.fetch(:default, 'uuid_generate_v4()')
- options[:primary_key] = true
- column name, type, options
- end
-
- def column(name, type = nil, options = {})
- super
- column = self[name]
- column.array = options[:array]
-
- self
- end
-
- private
-
- def create_column_definition(name, type)
- ColumnDefinition.new name, type
- end
- end
-
- class Table < ActiveRecord::ConnectionAdapters::Table
- include ColumnMethods
- end
-
ADAPTER_NAME = 'PostgreSQL'
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" },
decimal: { name: "decimal" },
datetime: { name: "timestamp" },
- timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
daterange: { name: "daterange" },
@@ -441,13 +102,20 @@ module ActiveRecord
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" },
- ltree: { name: "ltree" }
+ ltree: { name: "ltree" },
+ citext: { name: "citext" },
+ point: { name: "point" },
+ bit: { name: "bit" },
+ bit_varying: { name: "bit varying" },
+ money: { name: "money" },
}
- include Quoting
- include ReferentialIntegrity
- include SchemaStatements
- include DatabaseStatements
+ OID = PostgreSQL::OID #:nodoc:
+
+ include PostgreSQL::Quoting
+ include PostgreSQL::ReferentialIntegrity
+ include PostgreSQL::SchemaStatements
+ include PostgreSQL::DatabaseStatements
include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
@@ -455,9 +123,13 @@ module ActiveRecord
ADAPTER_NAME
end
+ def schema_creation # :nodoc:
+ PostgreSQL::SchemaCreation.new self
+ end
+
# Adds `:array` option to the default set provided by the
# AbstractAdapter
- def prepare_column_options(column, types)
+ def prepare_column_options(column, types) # :nodoc:
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
spec[:default] = "\"#{column.default_function}\"" if column.default_function
@@ -487,6 +159,10 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -544,19 +220,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 })
@prepared_statements = true
- @visitor = Arel::Visitors::PostgreSQL.new self
else
- @visitor = unprepared_visitor
+ @prepared_statements = false
end
@connection_parameters, @config = connection_parameters, config
@@ -573,7 +245,7 @@ module ActiveRecord
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
- @type_map = OID::TypeMap.new
+ @type_map = Type::HashLookupTypeMap.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
@@ -592,10 +264,6 @@ module ActiveRecord
false
end
- def active_threadsafe?
- @connection.connect_poll != PG::PGRES_POLLING_FAILED
- end
-
# Close then reopen the connection.
def reconnect!
super
@@ -605,7 +273,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
@@ -637,10 +310,6 @@ module ActiveRecord
self.client_min_messages = old
end
- def supports_insert_with_returning?
- true
- end
-
def supports_ddl_transactions?
true
end
@@ -659,6 +328,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
@@ -675,14 +348,13 @@ module ActiveRecord
if supports_extensions?
res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled",
'SCHEMA'
- res.column_types['enabled'].type_cast res.rows.first.first
+ res.cast_values.first
end
end
def extensions
if supports_extensions?
- res = exec_query "SELECT extname from pg_extension", "SCHEMA"
- res.rows.map { |r| res.column_types['extname'].type_cast r.first }
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
else
super
end
@@ -699,25 +371,6 @@ module ActiveRecord
exec_query "SET SESSION AUTHORIZATION #{user}"
end
- module Utils
- extend self
-
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
- # +schema_name+ is nil if not specified in +name+.
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
- #
- # * <tt>table_name</tt>
- # * <tt>"table.name"</tt>
- # * <tt>schema_name.table_name</tt>
- # * <tt>schema_name."table.name"</tt>
- # * <tt>"schema.name"."table name"</tt>
- def extract_schema_and_table(name)
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
- [schema, table]
- end
- end
-
def use_insert_returning?
@use_insert_returning
end
@@ -727,7 +380,12 @@ module ActiveRecord
end
def update_table_definition(table_name, base) #:nodoc:
- Table.new(table_name, base)
+ PostgreSQL::Table.new(table_name, base)
+ end
+
+ def lookup_cast_type(sql_type) # :nodoc:
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
+ super(oid)
end
protected
@@ -756,63 +414,163 @@ module ActiveRecord
private
- def type_map
- @type_map
- end
+ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
+ if !type_map.key?(oid)
+ load_additional_types(type_map, [oid])
+ end
- def reload_type_map
- type_map.clear
- initialize_type_map(type_map)
+ type_map.fetch(oid, fmod, sql_type) {
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
+ Type::Value.new.tap do |cast_type|
+ type_map.register_type(oid, cast_type)
+ end
+ }
end
- def add_oid(row, records_by_oid, type_map)
- return type_map if type_map.key? row['type_elem'].to_i
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, 'int2', OID::Integer
+ m.alias_type 'int4', 'int2'
+ m.alias_type 'int8', 'int2'
+ m.alias_type 'oid', 'int2'
+ m.register_type 'float4', OID::Float.new
+ m.alias_type 'float8', 'float4'
+ m.register_type 'text', Type::Text.new
+ register_class_with_limit m, 'varchar', Type::String
+ m.alias_type 'char', 'varchar'
+ m.alias_type 'name', 'varchar'
+ m.alias_type 'bpchar', 'varchar'
+ m.register_type 'bool', Type::Boolean.new
+ register_class_with_limit m, 'bit', OID::Bit
+ register_class_with_limit m, 'varbit', OID::BitVarying
+ m.alias_type 'timestamptz', 'timestamp'
+ m.register_type 'date', OID::Date.new
+ m.register_type 'time', OID::Time.new
+
+ m.register_type 'money', OID::Money.new
+ m.register_type 'bytea', OID::Bytea.new
+ m.register_type 'point', OID::Point.new
+ m.register_type 'hstore', OID::Hstore.new
+ m.register_type 'json', OID::Json.new
+ m.register_type 'jsonb', OID::Jsonb.new
+ m.register_type 'cidr', OID::Cidr.new
+ m.register_type 'inet', OID::Inet.new
+ m.register_type 'uuid', OID::Uuid.new
+ m.register_type 'xml', OID::Xml.new
+ m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
+ m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
+ m.register_type 'citext', OID::SpecializedString.new(:citext)
+ m.register_type 'ltree', OID::SpecializedString.new(:ltree)
+
+ # FIXME: why are we keeping these types as strings?
+ m.alias_type 'interval', 'varchar'
+ m.alias_type 'path', 'varchar'
+ m.alias_type 'line', 'varchar'
+ m.alias_type 'polygon', 'varchar'
+ m.alias_type 'circle', 'varchar'
+ m.alias_type 'lseg', 'varchar'
+ m.alias_type 'box', 'varchar'
+
+ m.register_type 'timestamp' do |_, _, sql_type|
+ precision = extract_precision(sql_type)
+ OID::DateTime.new(precision: precision)
+ end
- 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
+ m.register_type 'numeric' do |_, fmod, sql_type|
+ precision = extract_precision(sql_type)
+ scale = extract_scale(sql_type)
+
+ # The type for the numeric depends on the width of the field,
+ # so we'll do something special here.
+ #
+ # When dealing with decimal columns:
+ #
+ # places after decimal = fmod - 4 & 0xffff
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
+ if fmod && (fmod - 4 & 0xffff).zero?
+ # FIXME: Remove this class, and the second argument to
+ # lookups on PG
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ OID::Decimal.new(precision: precision, scale: scale)
end
-
- vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i]
end
- type_map[row['oid'].to_i] = vector
- type_map
+ load_additional_types(m)
end
- def initialize_type_map(type_map)
- result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
- leaves, nodes = result.partition { |row| row['typelem'] == '0' }
+ def extract_limit(sql_type) # :nodoc:
+ case sql_type
+ when /^bigint/i; 8
+ when /^smallint/i; 2
+ else super
+ end
+ end
- # populate the leaf nodes
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- type_map[row['oid'].to_i] = OID::NAMES[row['typname']]
+ # Extracts the value from a PostgreSQL column default definition.
+ def extract_value_from_default(oid, default) # :nodoc:
+ case default
+ # Quoted types
+ when /\A[\(B]?'(.*)'::/m
+ $1.gsub(/''/, "'")
+ # Boolean types
+ when 'true', 'false'
+ default
+ # Numeric types
+ when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\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
- records_by_oid = result.group_by { |row| row['oid'] }
+ def extract_default_function(default_value, default) # :nodoc:
+ default if has_default_function?(default_value, default)
+ end
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ def has_default_function?(default_value, default) # :nodoc:
+ !default_value && (%r{\w+\(.*\)} === default)
+ end
- # populate composite types
- nodes.each do |row|
- add_oid row, records_by_oid, type_map
+ def load_additional_types(type_map, oids = nil) # :nodoc:
+ 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
- # populate array types
- 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
+ if oids
+ query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
end
+
+ initializer = OID::TypeMapInitializer.new(type_map)
+ records = execute(query, 'SCHEMA')
+ initializer.run(records)
end
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, name, binds)
- log(sql, name, binds) { @connection.async_exec(sql) }
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
end
def exec_cache(sql, name, binds)
@@ -870,11 +628,6 @@ module ActiveRecord
@statements[sql_key]
end
- # The internal PostgreSQL identifier of the money data type.
- MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
- # The internal PostgreSQL identifier of the BYTEA data type.
- BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
-
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
@@ -883,14 +636,14 @@ module ActiveRecord
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
# should know about this but can't detect it there, so deal with it here.
- PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
+ OID::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)
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
else
- raise error
+ raise
end
end
@@ -965,7 +718,7 @@ module ActiveRecord
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
- def column_definitions(table_name) #:nodoc:
+ def column_definitions(table_name) # :nodoc:
exec_query(<<-end_sql, 'SCHEMA').rows
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
@@ -977,23 +730,13 @@ module ActiveRecord
end_sql
end
- def extract_pg_identifier_from_name(name)
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
-
- if match_data
- rest = name[match_data[0].length, name.length]
- rest = rest[1, rest.length] if rest.start_with? "."
- [match_data[1], (rest.length > 0 ? rest : nil)]
- end
- end
-
- def extract_table_ref_from_insert_sql(sql)
+ def extract_table_ref_from_insert_sql(sql) # :nodoc:
sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def create_table_definition(name, temporary, options, as = nil)
- TableDefinition.new native_database_types, name, temporary, options, as
+ def create_table_definition(name, temporary, options, as = nil) # :nodoc:
+ PostgreSQL::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 e5c9f6f54a..a10ce330c7 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 @@
-
module ActiveRecord
module ConnectionAdapters
class SchemaCache
@@ -12,15 +11,15 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@tables = {}
- prepare_default_proc
end
def primary_keys(table_name)
- @primary_keys[table_name]
+ @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
def table_exists?(name)
+ prepare_tables if @tables.empty?
return @tables[name] if @tables.key? name
@tables[name] = connection.table_exists?(name)
@@ -29,9 +28,9 @@ module ActiveRecord
# Add internal cache for table with +table_name+.
def add(table_name)
if table_exists?(table_name)
- @primary_keys[table_name]
- @columns[table_name]
- @columns_hash[table_name]
+ primary_keys(table_name)
+ columns(table_name)
+ columns_hash(table_name)
end
end
@@ -40,14 +39,16 @@ module ActiveRecord
end
# Get the columns for a table
- def columns(table)
- @columns[table]
+ def columns(table_name)
+ @columns[table_name] ||= connection.columns(table_name)
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
- def columns_hash(table)
- @columns_hash[table]
+ def columns_hash(table_name)
+ @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
+ [col.name, col]
+ }]
end
# Clears out internal caches
@@ -76,33 +77,18 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val|
- Hash[val]
- }
+ [@version, @columns, @columns_hash, @primary_keys, @tables]
end
def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @tables = array
- prepare_default_proc
end
private
- def prepare_default_proc
- @columns.default_proc = Proc.new do |h, table_name|
- h[table_name] = connection.columns(table_name)
- end
-
- @columns_hash.default_proc = Proc.new do |h, table_name|
- h[table_name] = Hash[columns(table_name).map { |col|
- [col.name, col]
- }]
+ def prepare_tables
+ connection.tables.each { |table| @tables[table] = true }
end
-
- @primary_keys.default_proc = Proc.new do |h, table_name|
- h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 3c5f7a981e..faf1cdc686 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -14,9 +14,9 @@ module ActiveRecord
raise ArgumentError, "No database file specified. Missing argument: database"
end
- # 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.
+ # 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 ':memory:' != config[:database]
config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
dirname = File.dirname(config[:database])
@@ -30,24 +30,32 @@ module ActiveRecord
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)
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
else
- raise error
+ raise
end
end
end
module ConnectionAdapters #:nodoc:
- class SQLite3Column < Column #:nodoc:
- class << self
- def binary_to_string(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
+ class SQLite3Binary < Type::Binary # :nodoc:
+ def cast_value(value)
+ if value.encoding != Encoding::ASCII_8BIT
+ value = value.force_encoding(Encoding::ASCII_8BIT)
+ end
+ value
+ end
+ end
+
+ class SQLite3String < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
+ value.encode(Encoding::UTF_8)
+ else
+ super
end
end
end
@@ -63,13 +71,12 @@ module ActiveRecord
NATIVE_DATABASE_TYPES = {
primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
- string: { name: "varchar", limit: 255 },
+ 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" },
@@ -123,11 +130,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
@@ -135,11 +138,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 })
@prepared_statements = true
- @visitor = Arel::Visitors::SQLite.new self
else
- @visitor = unprepared_visitor
+ @prepared_statements = false
end
end
@@ -225,10 +229,19 @@ module ActiveRecord
# QUOTING ==================================================
- def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary
- s = value.unpack("H*")[0]
- "x'#{s}'"
+ def _quote(value) # :nodoc:
+ case value
+ when Type::Binary::Data
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ end
+
+ def _type_cast(value) # :nodoc:
+ case value
+ when BigDecimal
+ value.to_f
else
super
end
@@ -256,24 +269,11 @@ module ActiveRecord
end
end
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
- value = value.encode Encoding::UTF_8
- end
- value
- end
-
# DATABASE STATEMENTS ======================================
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
@@ -299,9 +299,12 @@ module ActiveRecord
# 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] ||= {
@@ -382,7 +385,7 @@ module ActiveRecord
table_name && tables(nil, table_name).any?
end
- # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) #:nodoc:
table_structure(table_name).map do |field|
case field["dflt_value"]
@@ -394,7 +397,9 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
- SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
+ sql_type = field['type']
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
end
end
@@ -495,14 +500,19 @@ 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
+
+ def initialize_type_map(m)
+ super
+ m.register_type(/binary/i, SQLite3Binary.new)
+ register_class_with_limit m, %r(char)i, SQLite3String
+ end
+
def select(sql, name = nil, binds = []) #:nodoc:
exec_query(sql, name, binds)
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 11f6a47158..31e7390bf7 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -18,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"
# )
#
@@ -58,9 +58,9 @@ module ActiveRecord
end
class MergeAndResolveDefaultUrlConfig # :nodoc:
- def initialize(raw_configurations, url = ENV['DATABASE_URL'])
+ def initialize(raw_configurations)
@raw_config = raw_configurations.dup
- @url = url
+ @env = DEFAULT_ENV.call.to_s
end
# Returns fully resolved connection hashes.
@@ -71,38 +71,11 @@ module ActiveRecord
private
def config
- if @url
- raw_merged_into_default
- else
- @raw_config
- end
- end
-
- def raw_merged_into_default
- default = default_url_hash
-
- @raw_config.each do |env, values|
- default[env] = values || {}
- default[env].merge!("url" => @url) { |h, v1, v2| v1 || v2 } if default[env].is_a?(Hash)
- end
- default
- end
-
- # When the raw configuration is not present and ENV['DATABASE_URL']
- # is available we return a hash with the connection information in
- # the connection URL. This hash responds to any string key with
- # resolved connection information.
- def default_url_hash
- if @raw_config.blank?
- Hash.new do |hash, key|
- hash[key] = if key.is_a? String
- ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(@url).to_hash
- else
- nil
- end
+ @raw_config.dup.tap do |cfg|
+ if url = ENV['DATABASE_URL']
+ cfg[@env] ||= {}
+ cfg[@env]["url"] ||= url
end
- else
- {}
end
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index d9aaf8597f..d22806fbdf 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -16,7 +16,6 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
- # :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -94,6 +93,7 @@ module ActiveRecord
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
@@ -107,6 +107,76 @@ module ActiveRecord
end
module ClassMethods
+ def allocate
+ define_attribute_methods
+ super
+ end
+
+ 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 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
+
+ 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
+
def initialize_generated_modules
super
@@ -183,16 +253,12 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
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)
- @column_types_override = nil
- @column_types = self.class.column_types
+ @attributes = self.class.default_attributes.dup
init_internals
initialize_internals_callback
+ self.class.define_attribute_methods
# +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
@@ -212,13 +278,13 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# post.title # => 'hello world'
def init_with(coder)
- @attributes = self.class.initialize_attributes(coder['attributes'])
- @column_types_override = coder['column_types']
- @column_types = self.class.column_types
+ @attributes = coder['attributes']
init_internals
- @new_record = false
+ @new_record = coder['new_record']
+
+ self.class.define_attribute_methods
run_callbacks :find
run_callbacks :initialize
@@ -254,19 +320,16 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- self.class.initialize_attributes(cloned_attributes, :serialized => false)
-
- @attributes = cloned_attributes
- @attributes[self.class.primary_key] = nil
+ @attributes = @attributes.dup
+ @attributes.reset(self.class.primary_key)
run_callbacks(:initialize) unless _initialize_callbacks.empty?
@aggregation_cache = {}
@association_cache = {}
- @attributes_cache = {}
@new_record = true
+ @destroyed = false
super
end
@@ -284,7 +347,10 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- coder['attributes'] = attributes_for_coder
+ # FIXME: Remove this when we better serialize attributes
+ coder['raw_attributes'] = attributes_before_type_cast
+ coder['attributes'] = @attributes
+ coder['new_record'] = new_record?
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -299,7 +365,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id &&
+ !id.nil? &&
comparison_object.id == id
end
alias :eql? :==
@@ -307,7 +373,11 @@ module ActiveRecord
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
- id.hash
+ if id
+ id.hash
+ else
+ super
+ end
end
# Clone and freeze the attributes hash such that associations are still
@@ -363,6 +433,29 @@ module ActiveRecord
"#<#{self.class} #{inspection}>"
end
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
+ # when pp is required.
+ def pretty_print(pp)
+ pp.object_address_group(self) do
+ if defined?(@attributes) && @attributes
+ column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
+ pp.seplist(column_names, proc { pp.text ',' }) do |column_name|
+ column_value = read_attribute(column_name)
+ pp.breakable ' '
+ pp.group(1) do
+ pp.text column_name
+ pp.text ':'
+ pp.breakable
+ pp.pp column_value
+ end
+ end
+ else
+ pp.breakable ' '
+ pp.text 'not initialized'
+ end
+ end
+ end
+
# 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
@@ -426,12 +519,10 @@ module ActiveRecord
end
def init_internals
- pk = self.class.primary_key
- @attributes[pk] = nil unless @attributes.key?(pk)
+ @attributes.ensure_initialized(self.class.primary_key)
@aggregation_cache = {}
@association_cache = {}
- @attributes_cache = {}
@readonly = false
@destroyed = false
@marked_for_destruction = false
@@ -451,5 +542,11 @@ module ActiveRecord
def init_attributes(attributes, options)
assign_attributes(attributes)
end
+
+ def thaw
+ if frozen?
+ @attributes = @attributes.dup
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index dcbdf75627..f0b6afc4b4 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to reset a counter on.
- # * +counters+ - One or more association counters to reset
+ # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
#
# ==== Examples
#
@@ -19,9 +19,14 @@ module ActiveRecord
# Post.reset_counters(1, :comments)
def reset_counters(id, *counters)
object = find(id)
- counters.each do |association|
- has_many_association = reflect_on_association(association.to_sym)
- raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association
+ counters.each do |counter_association|
+ has_many_association = _reflect_on_association(counter_association.to_sym)
+ unless has_many_association
+ has_many = reflect_on_all_associations(:has_many)
+ has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
+ counter_association = has_many_association.plural_name if has_many_association
+ end
+ raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -29,12 +34,11 @@ module ActiveRecord
foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
- reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
+ reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
counter_name = reflection.counter_cache_column
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(association).count
+ arel_table[counter_name] => object.send(counter_association).count(:all)
}, primary_key)
connection.update stmt
end
@@ -118,5 +122,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/enum.rb b/activerecord/lib/active_record/enum.rb
index 4aa323fb00..f0ee433d0b 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -1,3 +1,5 @@
+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:
@@ -25,8 +27,10 @@ module ActiveRecord
# 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.
+ # as well. With the above example:
+ #
+ # Conversation.active
+ # Conversation.archived
#
# You can set the default value from the database declaration, like:
#
@@ -65,10 +69,14 @@ module ActiveRecord
#
# Where conditions on an enum attribute must use the ordinal value of an enum.
module Enum
- DEFINED_ENUMS = {} # :nodoc:
+ def self.extended(base) # :nodoc:
+ base.class_attribute(:defined_enums)
+ base.defined_enums = {}
+ end
- def enum_mapping_for(attr_name) # :nodoc:
- DEFINED_ENUMS[attr_name.to_s]
+ def inherited(base) # :nodoc:
+ base.defined_enums = defined_enums.deep_dup
+ super
end
def enum(definitions)
@@ -122,9 +130,8 @@ module ActiveRecord
klass.send(:detect_enum_conflict!, name, value, true)
klass.scope value, -> { klass.where name => i }
end
-
- DEFINED_ENUMS[name.to_s] = enum_values
end
+ defined_enums[name.to_s] = enum_values
end
end
@@ -133,17 +140,14 @@ module ActiveRecord
@_enum_methods_module ||= begin
mod = Module.new do
private
- def save_changed_attribute(attr_name, value)
- if (mapping = self.class.enum_mapping_for(attr_name))
+ def save_changed_attribute(attr_name, old)
+ if (mapping = self.class.defined_enums[attr_name.to_s])
+ value = read_attribute(attr_name)
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
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 7f6228131f..52c70977ef 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -30,17 +30,18 @@ module ActiveRecord
class SerializationTypeMismatch < ActiveRecordError
end
- # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
- # misses adapter field).
+ # Raised when adapter not specified on connection (or configuration file
+ # +config/database.yml+ misses adapter field).
class AdapterNotSpecified < ActiveRecordError
end
- # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically.
+ # Raised when Active Record cannot find database adapter specified in
+ # +config/database.yml+ or programmatically.
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
- # is given a nil object).
+ # Raised when connection to the database could not been established (for
+ # example when +connection=+ is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
@@ -82,35 +83,26 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
- # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
- # when using +find+ method)
- # does not match number of expected variables.
+ # Raised when number of bind variables in statement given to +:condition+ key
+ # (for example, when using +find+ method) does not match number of expected
+ # values supplied.
#
- # For example, in
+ # For example, when there are two placeholders with only one value supplied:
#
# Location.where("lat = ? AND lng = ?", 53.7362)
- #
- # two placeholders are given but only one variable to fill them.
class PreparedStatementInvalid < ActiveRecordError
end
- # Raised when a given database does not exist
- class NoDatabaseError < ActiveRecordError
- def initialize(message)
- super extend_message(message)
- end
-
- # can be over written to add additional error information.
- def extend_message(message)
- message
- 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.
#
- # Read more about optimistic locking in ActiveRecord::Locking module RDoc.
+ # Read more about optimistic locking in ActiveRecord::Locking module
+ # documentation.
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
@@ -122,8 +114,9 @@ module ActiveRecord
end
- # Raised when association is being configured improperly or
- # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
+ # Raised when association is being configured improperly or user tries to use
+ # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # associations.
class ConfigurationError < ActiveRecordError
end
@@ -161,7 +154,8 @@ module ActiveRecord
class Rollback < ActiveRecordError
end
- # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods).
+ # Raised when attribute has a name reserved by Active Record (when attribute
+ # has name of one of Active Record instance methods).
class DangerousAttributeError < ActiveRecordError
end
@@ -179,7 +173,7 @@ module ActiveRecord
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
+ # +attributes=+ method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
@@ -225,6 +219,13 @@ module ActiveRecord
class ImmutableRelation < ActiveRecordError
end
+ # TransactionIsolationError will be raised under the following conditions:
+ #
+ # * The adapter does not support setting the isolation level
+ # * You are joining an existing open transaction
+ # * You are creating a nested (savepoint) transaction
+ #
+ # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level.
class TransactionIsolationError < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index e65dab07ba..727a9befc1 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -27,7 +27,7 @@ module ActiveRecord
end.join("\n")
end.join("\n")
- # Overriding inspect to be more human readable, specially in the console.
+ # Overriding inspect to be more human readable, especially in the console.
def str.inspect
self
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 297792aeec..4306b36ae1 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/digest/uuid'
require 'active_record/fixture_set/file'
require 'active_record/errors'
@@ -14,9 +15,10 @@ module ActiveRecord
# They are stored in YAML files, one file per model, which are placed in the directory
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
- # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
- # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
- # like this:
+ # The fixture file ends with the +.yml+ file extension, for example:
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
+ #
+ # The format of a fixture file looks like this:
#
# rubyonrails:
# id: 1
@@ -32,7 +34,7 @@ module ActiveRecord
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
# separated by a blank line for your viewing pleasure.
#
- # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
+ # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
# See http://yaml.org/type/omap.html
# for the specification. You will need ordered fixtures when you have foreign key constraints
# on keys in the same table. This is commonly needed for tree structures. Example:
@@ -60,8 +62,8 @@ module ActiveRecord
# end
# end
#
- # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
- # so this test will succeed.
+ # By default, +test_helper.rb+ will load all of your fixtures into your test
+ # database, so this test will succeed.
#
# The testing environment will automatically load the all fixtures into the database before each
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
@@ -361,6 +363,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
@@ -372,8 +375,9 @@ module ActiveRecord
#
# == Support for YAML defaults
#
- # You probably already know how to use YAML to set and reuse defaults in
- # your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
+ # You can set and reuse defaults in your fixtures YAML file.
+ # This is the same technique used in the +database.yml+ file to specify
+ # defaults:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
@@ -389,7 +393,8 @@ module ActiveRecord
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
#--
- # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name.
+ # An instance of FixtureSet is normally stored in a single YAML file and
+ # possibly in a folder with the same name.
#++
MAX_ID = 2 ** 30 - 1
@@ -459,13 +464,7 @@ module ActiveRecord
@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)
- }
+ @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) }
end
def [](fs_name)
@@ -549,9 +548,13 @@ 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
+ Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
+ else
+ Zlib.crc32(label.to_s) % MAX_ID
+ end
end
# Superclass for the evaluation contexts used by ERB fixtures.
@@ -567,10 +570,6 @@ module ActiveRecord
@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
@@ -627,12 +626,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
@@ -643,19 +642,20 @@ module ActiveRecord
model_class
end
- reflection_class.reflect_on_all_associations.each do |association|
+ reflection_class._reflections.values.each do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
# support polymorphic belongs_to as "label (Type)"
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_many
if association.options[:through]
@@ -682,6 +682,10 @@ module ActiveRecord
def name
@association.name
end
+
+ def primary_key_type
+ @association.klass.column_types[@association.klass.primary_key].type
+ end
end
class HasManyThroughProxy < ReflectionProxy # :nodoc:
@@ -699,17 +703,22 @@ module ActiveRecord
@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
- lhs_key = association.lhs_key
- rhs_key = association.rhs_key
+ 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) }
+ rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
}
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 08fc91c9df..251d682a02 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,6 +1,37 @@
require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
+ # This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
+ # the companies table with type = "Firm". You can then fetch this row again using
+ # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
+ #
+ # Be aware that because the type column is an attribute on the record every new
+ # subclass will instantly be marked as dirty and the type column will be included
+ # in the list of changed attributes on the record. This is different from non
+ # STI classes:
+ #
+ # Company.new.changed? # => false
+ # Firm.new.changed? # => true
+ # Firm.new.changes # => {"type"=>["","Firm"]}
+ #
+ # If you don't have a type column defined in your table, single-table inheritance won't
+ # be triggered. In that case, it'll work just like normal subclasses with no special magic
+ # for differentiating between them or reloading the right type with find.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
module Inheritance
extend ActiveSupport::Concern
@@ -120,14 +151,8 @@ module ActiveRecord
candidates << type_name
candidates.each do |candidate|
- begin
- constant = ActiveSupport::Dependencies.constantize(candidate)
- return constant if candidate == constant.to_s
- # We don't want to swallow NoMethodError < NameError errors
- rescue NoMethodError
- raise
- rescue NameError
- end
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
+ return constant if candidate == constant.to_s
end
raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 6f54729b3c..52eeb8ae1f 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 = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -141,7 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
- @column_defaults = nil
+ clear_caches_calculated_from_columns
@locking_column = value.to_s
end
@@ -151,12 +151,6 @@ module ActiveRecord
@locking_column
end
- # 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
-
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
self.locking_column = DEFAULT_LOCKING_COLUMN
@@ -169,18 +163,42 @@ module ActiveRecord
super
end
- def column_defaults
- @column_defaults ||= begin
- defaults = super
-
- if defaults.key?(locking_column) && lock_optimistically
- defaults[locking_column] ||= 0
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ LockingType.new(type)
end
-
- defaults
end
+ super
end
end
end
+
+ class LockingType < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ # `nil` *should* be changed to 0
+ super.to_i
+ end
+
+ def changed?(old_value, *)
+ # Ensure we save if the default was `nil`
+ super || old_value == 0
+ end
+
+ def init_with(coder)
+ __setobj__(coder['subtype'])
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 654ef21b07..eb64d197f0 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -25,7 +25,7 @@ module ActiveRecord
if column.binary?
# This specifically deals with the PG adapter that casts bytea columns into a Hash.
value = value[:value] if value.is_a?(Hash)
- value = "<#{value.bytesize} bytes of binary data>"
+ value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
end
[column.name, value]
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index b6b02322d7..a6847e28c2 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -195,7 +195,7 @@ module ActiveRecord
# == Database support
#
# Migrations are currently supported in MySQL, PostgreSQL, SQLite,
- # SQL Server, Sybase, and Oracle (all supported databases except DB2).
+ # SQL Server, and Oracle (all supported databases except DB2).
#
# == More examples
#
@@ -372,13 +372,21 @@ module ActiveRecord
end
def call(env)
- mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
- if @last_check < mtime
- ActiveRecord::Migration.check_pending!
- @last_check = mtime
+ if connection.supports_migrations?
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
+ end
end
@app.call(env)
end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
end
class << self
@@ -391,7 +399,7 @@ module ActiveRecord
def load_schema_if_pending!
if ActiveRecord::Migrator.needs_migration?
- ActiveRecord::Tasks::DatabaseTasks.load_schema
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current
check_pending!
end
end
@@ -640,9 +648,11 @@ module ActiveRecord
say_with_time "#{method}(#{arg_list})" do
unless @connection.respond_to? :revert
- unless arguments.empty? || method == :execute
+ unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
- arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
+ if [:rename_table, :add_foreign_key].include?(method)
+ arguments[1] = proper_table_name(arguments.second, table_name_options)
+ end
end
end
return super unless connection.respond_to?(method)
@@ -711,7 +721,7 @@ module ActiveRecord
if ActiveRecord::Base.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
else
- "%.3d" % number
+ SchemaMigration.normalize_migration_number(number)
end
end
@@ -804,22 +814,22 @@ module ActiveRecord
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
- self.new(:up, migrations, target_version).migrate
+ new(:up, migrations, target_version).migrate
end
def down(migrations_paths, target_version = nil, &block)
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
- self.new(:down, migrations, target_version).migrate
+ new(:down, migrations, target_version).migrate
end
def run(direction, migrations_paths, target_version)
- self.new(direction, migrations(migrations_paths), target_version).run
+ new(direction, migrations(migrations_paths), target_version).run
end
def open(migrations_paths)
- self.new(:up, migrations(migrations_paths), nil)
+ new(:up, migrations(migrations_paths), nil)
end
def schema_migrations_table_name
@@ -851,19 +861,6 @@ module ActiveRecord
migrations(migrations_paths).last || NullMigration.new
end
- 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
- "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
- end
- end
-
def migrations_paths
@migrations_paths ||= ['db/migrate']
# just to not break things if someone uses: migration_path = some_string
@@ -895,7 +892,7 @@ module ActiveRecord
private
def move(direction, migrations_paths, steps)
- migrator = self.new(direction, migrations(migrations_paths))
+ migrator = new(direction, migrations(migrations_paths))
start_index = migrator.migrations.index(migrator.current_migration)
if start_index
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index c44d8c1665..36256415df 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -74,7 +74,9 @@ module ActiveRecord
: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, :enable_extension,
- :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too
+ :change_column, :execute, :remove_columns, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ # 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)
@@ -85,7 +87,7 @@ module ActiveRecord
alias :add_belongs_to :add_reference
alias :remove_belongs_to :remove_reference
- def change_table(table_name, options = {})
+ def change_table(table_name, options = {}) # :nodoc:
yield delegate.update_table_definition(table_name, self)
end
@@ -167,6 +169,21 @@ module ActiveRecord
[:change_column_null, args]
end
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
+
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
+
+ [:remove_foreign_key, [from_table, options]]
+ end
+
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if @delegate.respond_to?(method)
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index ebf64cbcdc..05569fadbd 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def join_table_name(table_1, table_2)
- [table_1.to_s, table_2.to_s].sort.join("_").to_sym
+ ModelSchema.derive_join_table_name(table_1, table_2).to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index dc5ff02882..850220babd 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -29,6 +29,10 @@ module ActiveRecord
# :singleton-method:
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
# "people_basecamp"). By default, the suffix is the empty string.
+ #
+ # If you are organising your models within modules, you can add a suffix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_suffix which
+ # returns your chosen suffix.
class_attribute :table_name_suffix, instance_writer: false
self.table_name_suffix = ""
@@ -47,6 +51,19 @@ module ActiveRecord
self.pluralize_table_names = true
self.inheritance_column = 'type'
+
+ delegate :type_for_attribute, to: :class
+ end
+
+ # Derives the join table name for +first_table+ and +second_table+. The
+ # table names appear in alphabetical order. A common prefix is removed
+ # (useful for namespaced models like Music::Artist and Music::Record):
+ #
+ # artists, records => artists_records
+ # records, artists => artists_records
+ # music_artists, music_records => music_artists_records
+ def self.derive_join_table_name(first_table, second_table) # :nodoc:
+ [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
end
module ClassMethods
@@ -153,6 +170,10 @@ module ActiveRecord
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
+ def full_table_name_suffix #:nodoc:
+ (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
+ end
+
# Defines the name of the table column which will store the class name on single-table
# inheritance situations.
#
@@ -190,7 +211,7 @@ module ActiveRecord
# given block. This is required for Oracle and is useful for any
# database which relies on sequences for primary key generation.
#
- # If a sequence name is not explicitly set when using Oracle or Firebird,
+ # If a sequence name is not explicitly set when using Oracle,
# it will default to the commonly used pattern of: #{table_name}_seq
#
# If a sequence name is not explicitly set when using PostgreSQL, it
@@ -209,50 +230,29 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name)
end
- # Returns an array of column objects for the table associated with this class.
- def columns
- @columns ||= connection.schema_cache.columns(table_name).map do |col|
- col = col.dup
- col.primary = (col.name == primary_key)
- col
- end
- end
-
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ def attributes_builder # :nodoc:
+ @attributes_builder ||= AttributeSet::Builder.new(column_types)
end
def column_types # :nodoc:
- @column_types ||= decorate_columns(columns_hash.dup)
- end
-
- def decorate_columns(columns_hash) # :nodoc:
- return if columns_hash.empty?
-
- @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])
+ @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
+ h.default = Type::Value.new
end
+ end
- columns_hash
+ def type_for_attribute(attr_name) # :nodoc:
+ column_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
- @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ default_attributes.to_hash
+ end
+
+ def default_attributes # :nodoc:
+ @default_attributes ||= attributes_builder.build_from_database(
+ columns_hash.transform_values(&:default))
end
# Returns an array of column names as strings.
@@ -263,7 +263,7 @@ module ActiveRecord
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed.
def content_columns
- @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
# Resets all the cached information about columns, which will cause them
@@ -298,27 +298,17 @@ module ActiveRecord
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
+ @default_attributes = 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
- # attributes when they are initialized. (e.g. attribute
- # serialization)
- def initialize_attributes(attributes, options = {}) #:nodoc:
- attributes
- end
-
private
# Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -337,7 +327,8 @@ module ActiveRecord
contained = contained.singularize if parent.pluralize_table_names
contained += '_'
end
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
+
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
base.table_name
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 9d92e747d4..8a2a06f2ca 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -305,7 +305,7 @@ module ActiveRecord
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
attr_names.each do |association_name|
- if reflection = reflect_on_association(association_name)
+ if reflection = _reflect_on_association(association_name)
reflection.autosave = true
add_autosave_association_callbacks(reflection)
@@ -485,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
@@ -516,10 +516,10 @@ module ActiveRecord
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
- ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
+ Type::Boolean.new.type_cast_from_user(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)
@@ -542,7 +542,7 @@ module ActiveRecord
end
def raise_nested_attributes_record_not_found!(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
end
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 5b255c3fe5..807c301596 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -23,7 +23,7 @@ module ActiveRecord
end
def size
- 0
+ calculate :size, nil
end
def empty?
@@ -43,18 +43,32 @@ module ActiveRecord
end
def count(*)
- 0
+ calculate :count, nil
end
def sum(*)
- 0
+ calculate :sum, nil
+ end
+
+ def average(*)
+ calculate :average, nil
+ end
+
+ def minimum(*)
+ calculate :minimum, nil
+ end
+
+ def maximum(*)
+ calculate :maximum, nil
end
def calculate(operation, _column_name, _options = {})
# TODO: Remove _options argument as soon we remove support to
# activerecord-deprecated_finders.
- if operation == :count
- 0
+ if [:count, :sum, :size].include? operation
+ group_values.any? ? Hash.new : 0
+ elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
+ Hash.new
else
nil
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index b1b35ed940..51b1931ed5 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -36,8 +36,25 @@ module ActiveRecord
end
end
+ # Creates an object (or multiple objects) and saves it to the database,
+ # if validations pass. Raises a RecordInvalid error if validations fail,
+ # unlike Base#create.
+ #
+ # The +attributes+ parameter can be either a Hash or an Array of Hashes.
+ # These describe which attributes to be created on the object, or
+ # multiple objects when given an Array of Hashes.
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ object = new(attributes, &block)
+ object.save!
+ object
+ end
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
- # the appropriate class.
+ # the appropriate class. Accepts only keys as strings.
#
# For example, +Post.all+ may return Comments, Messages, and Emails
# by storing the record's subclass in a +type+ attribute. By calling
@@ -46,10 +63,10 @@ module ActiveRecord
#
# See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
# how this "single-table" inheritance mapping is implemented.
- def instantiate(record, column_types = {})
- klass = discriminate_class_for_record(record)
- column_types = klass.decorate_columns(column_types.dup)
- klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
+ def instantiate(attributes, column_types = {})
+ klass = discriminate_class_for_record(attributes)
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end
private
@@ -64,7 +81,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
@@ -180,7 +197,6 @@ module ActiveRecord
def becomes(klass)
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?)
@@ -214,6 +230,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)
@@ -269,7 +287,8 @@ 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, "cannot update on a new record object" unless persisted?
+ raise ActiveRecordError, "cannot update a new record" if new_record?
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -394,24 +413,24 @@ module ActiveRecord
self.class.unscoped { self.class.find(id) }
end
- @attributes.update(fresh_object.instance_variable_get('@attributes'))
-
- @column_types = self.class.column_types
- @column_types_override = fresh_object.instance_variable_get('@column_types_override')
- @attributes_cache = {}
+ @attributes = fresh_object.instance_variable_get('@attributes')
+ @new_record = false
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and only the +after_touch+
- # callback is 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
@@ -430,11 +449,11 @@ module ActiveRecord
# ball = Ball.new
# ball.touch(:updated_at) # => raises ActiveRecordError
#
- def touch(name = nil)
+ 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
@@ -450,6 +469,8 @@ module ActiveRecord
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
@@ -477,24 +498,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 = self.attribute_names)
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 = self.attribute_names)
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/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index df8654e5c1..dcb2bd3d84 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Query Cache
class QueryCache
@@ -29,9 +28,10 @@ module ActiveRecord
end
def call(env)
- enabled = ActiveRecord::Base.connection.query_cache_enabled
+ connection = ActiveRecord::Base.connection
+ enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
- ActiveRecord::Base.connection.enable_query_cache!
+ connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index ef138c6f80..a9ddd9141f 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -37,14 +37,7 @@ module ActiveRecord
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
- column_types = {}
-
- if result_set.respond_to? :column_types
- column_types = result_set.column_types
- else
- ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
- end
-
+ column_types = result_set.column_types.except(*columns_hash.keys)
result_set.map { |record| instantiate(record, column_types) }
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 11b564f8f9..a4ceacbf44 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -116,17 +116,22 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
+ self.configurations = Rails.application.config.database_configuration
- class ActiveRecord::NoDatabaseError
- remove_possible_method :extend_message
- def extend_message(message)
- message << "Run `$ bin/rake db:create db:migrate` to create your database"
- message
- end
- end
+ begin
+ establish_connection
+ rescue ActiveRecord::NoDatabaseError
+ warn <<-end_warning
+Oops - You have a database configured, but it doesn't exist yet!
- self.configurations = Rails.application.config.database_configuration
- establish_connection
+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 1d5c80bc01..458862a538 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -28,12 +28,20 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.drop_current
end
+ namespace :purge do
+ task :all => :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
+ # desc "Empty 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 purging the development and test databases."
+ task :purge => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current
+ end
+
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
- ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
- ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
- end
+ ActiveRecord::Tasks::DatabaseTasks.migrate
db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration
end
@@ -82,22 +90,21 @@ db_namespace = namespace :db do
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
- puts 'Schema migrations table does not exist yet.'
- next # means "return" for rake task
+ unless ActiveRecord::SchemaMigration.table_exists?
+ abort 'Schema migrations table does not exist yet.'
end
- db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
- db_list.map! { |version| "%.3d" % version }
- file_list = []
- ActiveRecord::Migrator.migrations_paths.each do |path|
- Dir.foreach(path) do |file|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- if match_data = /^(\d{3,})_(.+)\.rb$/.match(file)
- status = db_list.delete(match_data[1]) ? 'up' : 'down'
- file_list << [status, match_data[1], match_data[2].humanize]
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list =
+ ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
+ Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
+ version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
+ status = db_list.delete(version) ? 'up' : 'down'
+ [status, version, $2.humanize]
+ end
end
- end
- end
+
db_list.map! do |version|
['up', version, '********** NO FILE **********']
end
@@ -105,8 +112,8 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
- puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}"
+ (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
end
@@ -178,17 +185,21 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = if ENV['FIXTURES_PATH']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
+ fixtures_dir = if ENV['FIXTURES_DIR']
+ File.join base_dir, ENV['FIXTURES_DIR']
+ else
+ base_dir
+ end
- (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
+ fixture_files = if ENV['FIXTURES']
+ ENV['FIXTURES'].split(',')
+ else
+ Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s }
+ end
+
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
@@ -200,12 +211,7 @@ 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']
- File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
- else
- ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- end
-
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
@@ -234,7 +240,7 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA'])
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -268,7 +274,8 @@ 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"
@@ -277,9 +284,9 @@ db_namespace = namespace :db do
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
- ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE'])
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -310,9 +317,8 @@ db_namespace = namespace :db 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
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA']
ensure
if should_reconnect
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
@@ -322,12 +328,7 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent structure.sql file"
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
- ensure
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil)
- end
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA']
end
# desc "Recreate the test database from a fresh schema"
@@ -366,7 +367,7 @@ namespace :railties do
task :migrations => :'db:load_config' do
to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
railties = {}
- Rails.application.railties.each do |railtie|
+ Rails.application.migration_railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first)
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index b3ddfd63d4..85bbac43e4 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module ReadonlyAttributes
extend ActiveSupport::Concern
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index bce7766501..1547c8e3f4 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,35 +1,44 @@
+require 'thread'
+
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
extend ActiveSupport::Concern
included do
- class_attribute :reflections
+ class_attribute :_reflections
class_attribute :aggregate_reflections
- self.reflections = {}
+ self._reflections = {}
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)
+ klass = case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+
+ reflection = klass.new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
end
def self.add_reflection(ar, name, reflection)
- ar.reflections = ar.reflections.merge(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 => reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
end
- # \Reflection enables to interrogate Active Record classes and objects
+ # \Reflection enables interrogating 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
# and creates input fields for all of the attributes depending on their type
@@ -48,7 +57,25 @@ 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 a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ #
+ # Account.reflections # => {balance: AggregateReflection}
+ #
+ # @api public
+ def reflections
+ ref = {}
+ _reflections.each do |name, reflection|
+ parent_name, parent_reflection = reflection.parent_reflection
+ if parent_name
+ ref[parent_name] = parent_reflection
+ else
+ ref[name] = reflection
+ end
+ end
+ ref
end
# Returns an array of AssociationReflection objects for all the
@@ -61,6 +88,7 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
+ # @api public
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
@@ -71,36 +99,82 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
+ # @api public
def reflect_on_association(association)
- reflections[association]
+ reflections[association.to_s]
+ end
+
+ # @api private
+ def _reflect_on_association(association) #:nodoc:
+ _reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
+ #
+ # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
end
+ # Holds all the methods that are shared between MacroReflection, AssociationReflection
+ # and ThroughReflection
+ class AbstractReflection # :nodoc:
+ def table_name
+ klass.table_name
+ end
+
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
+ # be passed to the class's constructor.
+ def build_association(attributes, &block)
+ klass.new(attributes, &block)
+ end
+
+ def quoted_table_name
+ klass.quoted_table_name
+ end
+
+ def primary_key_type
+ klass.type_for_attribute(klass.primary_key)
+ end
+
+ # Returns the class name for the macro.
+ #
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
+ def class_name
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
+ end
+
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
+
+ def join_keys(assoc_klass)
+ JoinKeys.new(foreign_key, active_record_primary_key)
+ end
+
+ def source_macro
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \
+ "will be removed without replacement.")
+ macro
+ end
+ end
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
#
# MacroReflection
- # AggregateReflection
# AssociationReflection
- # ThroughReflection
- class MacroReflection
+ # AggregateReflection
+ # HasManyReflection
+ # HasOneReflection
+ # BelongsToReflection
+ # ThroughReflection
+ class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
- # Returns the macro type.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
- # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
- attr_reader :macro
-
attr_reader :scope
# Returns the hash of options used for the macro.
@@ -113,8 +187,7 @@ module ActiveRecord
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, scope, options, active_record)
- @macro = macro
+ def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@@ -127,6 +200,10 @@ module ActiveRecord
def autosave=(autosave)
@automatic_inverse_of = false
@options[:autosave] = autosave
+ _, parent_reflection = self.parent_reflection
+ if parent_reflection
+ parent_reflection.autosave = autosave
+ end
end
# Returns the class for the macro.
@@ -134,15 +211,11 @@ module ActiveRecord
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
def klass
- @klass ||= class_name.constantize
+ @klass ||= compute_class(class_name)
end
- # Returns the class name for the macro.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
- # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
- def class_name
- @class_name ||= (options[:class_name] || derive_class_name).to_s
+ def compute_class(name)
+ name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -151,7 +224,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
@@ -187,38 +260,40 @@ module ActiveRecord
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
- @klass ||= active_record.send(:compute_type, class_name)
+ @klass ||= compute_class(class_name)
+ end
+
+ def compute_class(name)
+ active_record.send(:compute_type, name)
end
attr_reader :type, :foreign_type
+ attr_accessor :parent_reflection # [:name, Reflection]
- def initialize(macro, name, scope, options, active_record)
+ def initialize(name, scope, options, active_record)
super
- @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
- # Returns a new, unsaved instance of the associated class. +attributes+ will
- # be passed to the class's constructor.
- def build_association(attributes, &block)
- klass.new(attributes, &block)
+ def association_scope_cache(conn, owner)
+ key = conn.prepared_statements
+ if polymorphic?
+ key = [key, owner.read_attribute(@foreign_type)]
+ end
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
+ @association_scope_cache[key] ||= yield
+ }
end
def constructable? # :nodoc:
@constructable
end
- def table_name
- klass.table_name
- end
-
- def quoted_table_name
- klass.quoted_table_name
- end
-
def join_table
@join_table ||= options[:join_table] || derive_join_table
end
@@ -227,10 +302,6 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def primary_key_column
- klass.columns_hash[klass.primary_key]
- end
-
def association_foreign_key
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
@@ -257,13 +328,31 @@ module ActiveRecord
end
def check_validity_of_inverse!
- unless options[:polymorphic]
+ unless polymorphic?
if has_inverse? && inverse_of.nil?
raise InverseOfAssociationNotFoundError.new(self)
end
end
end
+ def check_preloadable!
+ return unless scope
+
+ if scope.arity > 0
+ ActiveSupport::Deprecation.warn <<-WARNING
+The association scope '#{name}' is instance dependent (the scope block takes an argument).
+Preloading happens before the individual instances are created. This means that there is no instance
+being passed to the association scope. This will most likely result in broken or incorrect behavior.
+Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
+ WARNING
+ end
+ end
+ alias :check_eager_loadable! :check_preloadable!
+
+ def join_id_for(owner) # :nodoc:
+ owner[active_record_primary_key]
+ end
+
def through_reflection
nil
end
@@ -288,8 +377,6 @@ module ActiveRecord
scope ? [[scope]] : [[]]
end
- alias :source_macro :macro
-
def has_inverse?
inverse_name
end
@@ -297,12 +384,12 @@ module ActiveRecord
def inverse_of
return unless inverse_name
- @inverse_of ||= klass.reflect_on_association inverse_name
+ @inverse_of ||= klass._reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
if has_inverse?
- if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
+ if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
inverse_relationship
else
raise InverseOfAssociationNotFoundError.new(self, associated_class)
@@ -310,11 +397,16 @@ module ActiveRecord
end
end
+ # Returns the macro type.
+ #
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
+ def macro; raise NotImplementedError; end
+
# Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
- @collection
+ false
end
# Returns whether or not the association should be validated as part of
@@ -327,18 +419,19 @@ module ActiveRecord
# * you use autosave; <tt>autosave: true</tt>
# * the association is a +has_many+ association
def validate?
- !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
- def belongs_to?
- macro == :belongs_to
- end
+ def belongs_to?; false; end
+
+ # Returns +true+ if +self+ is a +has_one+ reflection.
+ def has_one?; false; end
def association_class
case macro
when :belongs_to
- if options[:polymorphic]
+ if polymorphic?
Associations::BelongsToPolymorphicAssociation
else
Associations::BelongsToAssociation
@@ -359,7 +452,7 @@ module ActiveRecord
end
def polymorphic?
- options.key? :polymorphic
+ options[:polymorphic]
end
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
@@ -376,7 +469,7 @@ module ActiveRecord
def calculate_constructable(macro, options)
case macro
when :belongs_to
- !options[:polymorphic]
+ !polymorphic?
when :has_one
!options[:through]
else
@@ -400,10 +493,10 @@ 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 = ActiveSupport::Inflector.underscore(active_record.name).to_sym
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym
begin
- reflection = klass.reflect_on_association(inverse_name)
+ reflection = klass._reflect_on_association(inverse_name)
rescue NameError
# Give up: we couldn't compute the klass type so we won't be able
# to find any associations either.
@@ -449,9 +542,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
@@ -465,7 +558,7 @@ module ActiveRecord
end
def derive_join_table
- [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
def primary_key(klass)
@@ -473,15 +566,72 @@ module ActiveRecord
end
end
+ class HasManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_many; end
+
+ def collection?; true; end
+ end
+
+ class HasOneReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_one; end
+
+ def has_one?; true; end
+ end
+
+ class BelongsToReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :belongs_to; end
+
+ def belongs_to?; true; end
+
+ def join_keys(assoc_klass)
+ key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key
+ JoinKeys.new(key, foreign_key)
+ end
+
+ def join_id_for(owner) # :nodoc:
+ owner[foreign_key]
+ end
+ end
+
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super
+ end
+
+ def macro; :has_and_belongs_to_many; end
+
+ def collection?
+ true
+ end
+ end
+
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
- class ThroughReflection < AssociationReflection #:nodoc:
+ class ThroughReflection < AbstractReflection #:nodoc:
+ attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- def initialize(macro, name, scope, options, active_record)
- super
- @source_reflection_name = options[:source]
+ def initialize(delegate_reflection)
+ @delegate_reflection = delegate_reflection
+ @klass = delegate_reflection.options[:class]
+ @source_reflection_name = delegate_reflection.options[:source]
+ end
+
+ def klass
+ @klass ||= delegate_reflection.compute_class(class_name)
end
# Returns the source of the through reflection. It checks both a singularized
@@ -499,10 +649,10 @@ module ActiveRecord
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.source_reflection
- # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
+ # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
- through_reflection.klass.reflect_on_association(source_reflection_name)
+ through_reflection.klass._reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
@@ -515,10 +665,10 @@ module ActiveRecord
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.through_reflection
- # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
+ # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
- active_record.reflect_on_association(options[:through])
+ active_record._reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
@@ -535,8 +685,8 @@ module ActiveRecord
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.chain
- # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>,
- # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>]
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
+ # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
#
def chain
@chain ||= begin
@@ -586,8 +736,14 @@ module ActiveRecord
end
end
+ def join_keys(assoc_klass)
+ source_reflection.join_keys(assoc_klass)
+ end
+
# The macro used by the source association
def source_macro
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \
+ "will be removed without replacement.")
source_reflection.source_macro
end
@@ -617,7 +773,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:
@@ -625,7 +781,7 @@ module ActiveRecord
names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
names = names.find_all { |n|
- through_reflection.klass.reflect_on_association(n)
+ through_reflection.klass._reflect_on_association(n)
}
if names.length > 1
@@ -653,12 +809,16 @@ directive on your declaration like:
through_reflection.options
end
+ def join_id_for(owner) # :nodoc:
+ source_reflection.join_id_for(owner)
+ end
+
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
- if through_reflection.options[:polymorphic]
+ if through_reflection.polymorphic?
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
end
@@ -666,15 +826,15 @@ directive on your declaration like:
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
- if options[:source_type] && source_reflection.options[:polymorphic].nil?
+ if options[:source_type] && !source_reflection.polymorphic?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
- if source_reflection.options[:polymorphic] && options[:source_type].nil?
+ if source_reflection.polymorphic? && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
- if macro == :has_one && through_reflection.collection?
+ if has_one? && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
@@ -683,15 +843,25 @@ directive on your declaration like:
protected
- def actual_source_reflection # FIXME: this is a horrible name
- source_reflection.actual_source_reflection
- end
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.send(:actual_source_reflection)
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
end
+
+ delegate_methods = AssociationReflection.public_instance_methods -
+ public_instance_methods
+
+ delegate(*delegate_methods, to: :delegate_reflection)
+
end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index fb213dc6f7..ad54d84665 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
@@ -11,6 +12,7 @@ module ActiveRecord
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
:reverse_order, :distinct, :create_with, :uniq]
+ INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
@@ -70,9 +72,16 @@ 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, @klass.primary_key)
+
+ scope = @klass.unscoped
+
+ if @klass.finder_needs_type_condition?
+ scope.unscope!(where: @klass.inheritance_column)
+ end
+
+ um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key)
@klass.connection.update(
um,
@@ -223,6 +232,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
@@ -232,13 +242,18 @@ module ActiveRecord
@records
end
+ # Serializes the relation objects Array.
+ def encode_with(coder)
+ coder.represent_seq(nil, to_a)
+ end
+
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
# Returns size of the records.
def size
- loaded? ? @records.length : count
+ loaded? ? @records.length : count(:all)
end
# Returns true if there are no records.
@@ -248,8 +263,7 @@ module ActiveRecord
if limit_value == 0
true
else
- # FIXME: This count is not compatible with #select('authors.*') or other select narrows
- c = count
+ c = count(:all)
c.respond_to?(:zero?) ? c.zero? : c.empty?
end
end
@@ -324,7 +338,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.
@@ -428,12 +443,21 @@ module ActiveRecord
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
#
- # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
+ # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
#
# Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
def delete_all(conditions = nil)
- raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
+ if MULTI_VALUE_METHODS.include?(method)
+ send("#{method}_values").any?
+ else
+ send("#{method}_value")
+ end
+ }
+ if invalid_methods.any?
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
+ end
if conditions
where(conditions).delete_all
@@ -517,11 +541,11 @@ module ActiveRecord
find_with_associations { |rel| relation = rel }
end
- ast = relation.arel.ast
- binds = relation.bind_values.dup
- visitor.accept(ast) do
- connection.quote(*binds.shift.reverse)
- 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
@@ -529,16 +553,22 @@ module ActiveRecord
#
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
- def where_values_hash
+ def where_values_hash(relation_table_name = table_name)
equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
- node.left.relation.name == table_name
+ node.left.relation.name == relation_table_name
}
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
@@ -570,6 +600,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
@@ -600,7 +632,7 @@ module ActiveRecord
private
def exec_queries
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
preload = preload_values
preload += includes_values unless eager_loading?
@@ -617,7 +649,9 @@ module ActiveRecord
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
- unless join.is_a?(Arel::Nodes::StringJoin)
+ if join.is_a?(Arel::Nodes::StringJoin)
+ tables_in_string(join.left)
+ else
[join.left.table_name, join.left.table_alias]
end
end
@@ -629,5 +663,12 @@ module ActiveRecord
(references_values - joined_tables).any?
end
+
+ def tables_in_string(string)
+ return [] if string.blank?
+ # always convert table names to downcase as in Oracle quoted table names are in uppercase
+ # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 29fc150b3d..b069cdce7c 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module Batches
# Looping through a collection of records from the database
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 45ffb99868..90e99957f6 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -19,6 +19,22 @@ module ActiveRecord
#
# Person.group(:city).count
# # => { 'Rome' => 5, 'Paris' => 3 }
+ #
+ # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
+ # keys are an array containing the individual values of each column and the value
+ # of each key would be the +count+.
+ #
+ # Article.group(:status, :category).count
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
+ # ["published", "business"]=>0, ["published", "technology"]=>2}
+ #
+ # If +count+ is used with +select+, it will count the selected columns:
+ #
+ # Person.select(:age).count
+ # # => counts the number of different age values
+ #
+ # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
+ # between databases. In invalid cases, an error from the database is thrown.
def count(column_name = nil, options = {})
# TODO: Remove options argument as soon we remove support to
# activerecord-deprecated_finders.
@@ -162,18 +178,7 @@ module ActiveRecord
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) { result.identity_type }
- }
- end
-
- result = result.map do |attributes|
- values = klass.initialize_attributes(attributes).values
-
- columns.zip(values).map { |column, value| column.type_cast value }
- end
- columns.one? ? result.map!(&:first) : result
+ result.cast_values(klass.column_types)
end
end
@@ -188,7 +193,7 @@ 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 = {})
@@ -231,15 +236,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)
@@ -249,13 +257,14 @@ 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
- column_for(column_name)
+ type_for(column_name)
end
type_cast_calculated_value(value, column, operation)
@@ -265,8 +274,8 @@ module ActiveRecord
group_attrs = group_values
if group_attrs.first.respond_to?(:to_sym)
- association = @klass.reflect_on_association(group_attrs.first.to_sym)
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
+ association = @klass._reflect_on_association(group_attrs.first.to_sym)
+ associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
group_fields = Array(associated ? association.foreign_key : group_attrs)
else
group_fields = group_attrs
@@ -318,14 +327,14 @@ module ActiveRecord
Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, col_name|
column = calculated_data.column_types.fetch(aliaz) do
- column_for(col_name)
+ type_for(col_name)
end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
key = key_records[key] if associated
- column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) }
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
[key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -352,24 +361,20 @@ module ActiveRecord
@klass.connection.table_alias_for(table_name)
end
- def column_for(field)
+ def type_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns_hash[field_name]
+ @klass.type_for_attribute(field_name)
end
- def type_cast_calculated_value(value, column, operation = nil)
+ def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || 0, column)
+ when 'sum' then type.type_cast_from_database(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type_cast_using_column(value, column)
+ else type.type_cast_from_database(value)
end
end
- def type_cast_using_column(value, column)
- 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?
@@ -385,9 +390,11 @@ module ActiveRecord
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 21beed332f..50f4d5c7ab 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -40,10 +40,10 @@ module ActiveRecord
BLACKLISTED_ARRAY_METHODS = [
:compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
:shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
- :keep_if, :pop, :shift, :delete_at, :compact
+ :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 :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :to => :klass
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 7099bdd285..0c9c761f97 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,3 +1,5 @@
+require 'active_support/deprecation'
+
module ActiveRecord
module FinderMethods
ONE_AS_ONE = '1 AS one'
@@ -63,7 +65,7 @@ module ActiveRecord
# # returns an Array of the required fields, available since Rails 3.1.
def find(*args)
if block_given?
- to_a.find { |*block_args| yield(*block_args) }
+ to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
@@ -127,9 +129,9 @@ module ActiveRecord
#
def first(limit = nil)
if limit
- find_nth_with_limit(offset_value, limit)
+ find_nth_with_limit(offset_index, limit)
else
- find_nth(:first, offset_value)
+ find_nth(0, offset_index)
end
end
@@ -179,7 +181,7 @@ module ActiveRecord
# 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(:second, offset_value ? offset_value + 1 : 1)
+ find_nth(1, offset_index)
end
# Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
@@ -195,7 +197,7 @@ module ActiveRecord
# 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(:third, offset_value ? offset_value + 2 : 2)
+ find_nth(2, offset_index)
end
# Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
@@ -211,7 +213,7 @@ module ActiveRecord
# 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(:fourth, offset_value ? offset_value + 3 : 3)
+ find_nth(3, offset_index)
end
# Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
@@ -227,7 +229,7 @@ module ActiveRecord
# 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(:fifth, offset_value ? offset_value + 4 : 4)
+ find_nth(4, offset_index)
end
# Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
@@ -240,10 +242,10 @@ module ActiveRecord
# 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 fifth object from OFFSET 3 (which is OFFSET 44)
+ # 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(:forty_two, offset_value ? offset_value + 41 : 41)
+ find_nth(41, offset_index)
end
# Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
@@ -280,7 +282,12 @@ module ActiveRecord
# 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 = apply_join_dependency(self, construct_join_dependency)
@@ -292,7 +299,9 @@ module ActiveRecord
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
connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
@@ -322,8 +331,21 @@ module ActiveRecord
private
+ def offset_index
+ offset_value || 0
+ end
+
def find_with_associations
- join_dependency = construct_join_dependency
+ # NOTE: the JoinDependency constructed here needs to know about
+ # any joins already present in `self`, so pass them in
+ #
+ # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
+ # incorrect SQL is generated. In that case, the join dependency for
+ # SpecialCategorizations is constructed without knowledge of the
+ # preexisting join in joins_values to categorizations (by way of
+ # the `has_many :through` for categories).
+ #
+ join_dependency = construct_join_dependency(joins_values)
aliases = join_dependency.aliases
relation = select aliases.columns
@@ -335,7 +357,8 @@ module ActiveRecord
if ActiveRecord::NullRelation === relation
[]
else
- rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ arel = relation.arel
+ rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
join_dependency.instantiate(rows, aliases)
end
end
@@ -409,7 +432,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)
@@ -452,20 +479,24 @@ module ActiveRecord
end
end
- def find_nth(ordinal, offset)
+ def find_nth(index, offset)
if loaded?
- @records.send(ordinal)
+ @records[index]
else
+ offset += index
@offsets[offset] ||= find_nth_with_limit(offset, 1).first
end
end
def find_nth_with_limit(offset, limit)
- if order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a
- else
- limit(limit).offset(offset).to_a
- end
+ 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
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 182b9ed89c..ac41d0aa80 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -30,6 +30,8 @@ module ActiveRecord
else
other.joins!(*v)
end
+ elsif k == :select
+ other._select!(v)
else
other.send("#{k}!", v)
end
@@ -62,7 +64,13 @@ module ActiveRecord
# 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.
- relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value)
+ unless value.nil? || (value.blank? && false != value)
+ if name == :select
+ relation._select!(*value)
+ else
+ relation.send("#{name}!", *value)
+ end
+ end
end
merge_multi_values
@@ -139,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])
@@ -149,7 +156,7 @@ module ActiveRecord
def filter_binds(lhs_binds, removed_wheres)
return lhs_binds if removed_wheres.empty?
- set = Set.new removed_wheres.map { |x| x.left.name }
+ set = Set.new removed_wheres.map { |x| x.left.name.to_s }
lhs_binds.dup.delete_if { |col,_| set.include? col.name }
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 1252af7635..eff5c8f09c 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -26,7 +26,7 @@ module ActiveRecord
queries << '1=0'
else
table = Arel::Table.new(column, default_table.engine)
- association = klass.reflect_on_association(column.to_sym)
+ association = klass._reflect_on_association(column.to_sym)
value.each do |k, v|
queries.concat expand(association && association.klass, table, k, v)
@@ -55,7 +55,7 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && reflection = klass.reflect_on_association(column.to_sym)
+ 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
@@ -113,13 +113,14 @@ module ActiveRecord
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new)
- private
- def self.build(attribute, value)
- handler_for(value).call(attribute, value)
- end
+ 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
+ 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
index 2f6c34ac08..78dba8be06 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -2,28 +2,33 @@ module ActiveRecord
class PredicateBuilder
class ArrayHandler # :nodoc:
def call(attribute, value)
+ return attribute.in([]) if value.empty?
+
values = value.map { |x| x.is_a?(Base) ? x.id : x }
ranges, values = values.partition { |v| v.is_a?(Range) }
+ nils, values = values.partition(&:nil?)
- values_predicate = if values.include?(nil)
- values = values.compact
-
+ values_predicate =
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))
+ when 0 then NullPredicate
+ when 1 then attribute.eq(values.first)
+ else attribute.in(values)
end
- else
- attribute.in(values)
+
+ unless nils.empty?
+ values_predicate = values_predicate.or(attribute.eq(nil))
end
array_predicates = ranges.map { |range| attribute.in(range) }
array_predicates << values_predicate
array_predicates.inject { |composite, predicate| composite.or(predicate) }
end
+
+ module NullPredicate
+ def self.or(other)
+ other
+ end
+ 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 077f09b67d..1262b2c291 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -49,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
@@ -62,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
@@ -79,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
@@ -168,7 +182,7 @@ module ActiveRecord
# 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 conjuction with +includes+.
+ # This method only works in conjunction with +includes+.
# See #includes for more details.
#
# User.includes(:posts).where("posts.name = 'foo'")
@@ -202,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
@@ -211,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:
#
@@ -224,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
@@ -233,14 +247,14 @@ 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:
+ def _select!(*fields) # :nodoc:
fields.flatten!
fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
+ klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
end
self.select_values += fields
self
@@ -261,6 +275,10 @@ 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)
spawn.group!(*args)
@@ -275,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
#
@@ -292,6 +301,15 @@ 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)
spawn.order!(*args)
@@ -555,15 +573,11 @@ module ActiveRecord
end
end
- def where!(opts = :chain, *rest) # :nodoc:
- if opts == :chain
- WhereChain.new(self)
- else
- references!(PredicateBuilder.references(opts)) if Hash === opts
+ def where!(opts, *rest) # :nodoc:
+ references!(PredicateBuilder.references(opts)) if Hash === opts
- self.where_values += build_where(opts, rest)
- self
- end
+ self.where_values += build_where(opts, rest)
+ self
end
# Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
@@ -819,7 +833,9 @@ 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
@@ -835,7 +851,7 @@ module ActiveRecord
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(&:blank?)) unless having_values.empty?
@@ -852,6 +868,15 @@ 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
@@ -865,8 +890,9 @@ module ActiveRecord
case scope
when :order
- self.reverse_order_value = false
result = []
+ when :where
+ self.bind_values = []
else
result = [] unless single_val_method
end
@@ -917,18 +943,15 @@ module ActiveRecord
def build_where(opts, other = [])
case opts
when String, Array
- #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
- values = Hash === other.first ? other.first.values : other
-
- values.grep(ActiveRecord::Relation) do |rel|
- self.bind_values += rel.bind_values
- end
-
[@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
@@ -939,6 +962,29 @@ 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
@@ -980,9 +1026,12 @@ module ActiveRecord
join_list
)
- joins = join_dependency.join_constraints stashed_association_joins
+ join_infos = join_dependency.join_constraints stashed_association_joins
- joins.each { |join| manager.from(join) }
+ 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)
@@ -995,8 +1044,6 @@ module ActiveRecord
columns_hash.key?(field.to_s) ? arel_table[field] : field
end
arel.project(*expanded_select)
- elsif from_value
- arel.project(Arel.star)
else
arel.project(@klass.arel_table[Arel.star])
end
@@ -1027,15 +1074,19 @@ module ActiveRecord
def build_order(arel)
orders = order_values.uniq
orders.reject!(&:blank?)
- orders = reverse_sql_order(orders) if reverse_order_value
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
@@ -1052,12 +1103,12 @@ module ActiveRecord
order_args.map! do |arg|
case arg
when Symbol
- arg = klass.attribute_alias(arg).to_sym if klass.attribute_alias?(arg)
+ arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
table[arg].asc
when Hash
arg.map { |field, dir|
- field = klass.attribute_alias(field).to_sym if klass.attribute_alias?(field)
- table[field].send(dir)
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ table[field].send(dir.downcase)
}
else
arg
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 2552cbd234..57d66bce4b 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -58,6 +58,9 @@ 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
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 228b2aa60f..8405fdaeb9 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -31,7 +31,7 @@ module ActiveRecord
class Result
include Enumerable
- IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc:
+ IDENTITY_TYPE = Type::Value.new # :nodoc:
attr_reader :columns, :rows, :column_types
@@ -42,14 +42,6 @@ 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 }
@@ -82,6 +74,15 @@ module ActiveRecord
hash_rows.last
end
+ def cast_values(type_overrides = {}) # :nodoc:
+ types = columns.map { |name| column_type(name, type_overrides) }
+ result = rows.map do |values|
+ types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ end
+
+ columns.one? ? result.map!(&:first) : result
+ end
+
def initialize_copy(other)
@columns = columns.dup
@rows = rows.dup
@@ -91,6 +92,12 @@ module ActiveRecord
private
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, IDENTITY_TYPE)
+ end
+ end
+
def hash_rows
@hash_rows ||=
begin
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 5a71c13d91..ff70cbed0f 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -92,7 +92,7 @@ module ActiveRecord
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
@@ -107,6 +107,13 @@ module ActiveRecord
end.join(', ')
end
+ # Sanitizes a +string+ so that it is safe to use within an 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'"
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 4bfd0167a4..0a5546a760 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Schema
#
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index e055d571ab..fae6427ea1 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -91,16 +91,17 @@ HEADER
end
def tables(stream)
- @connection.tables.sort.each do |tbl|
- next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
- case ignored
- when String; remove_prefix_and_suffix(tbl) == ignored
- when Regexp; remove_prefix_and_suffix(tbl) =~ ignored
- else
- raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
- end
+ sorted_tables = @connection.tables.sort
+
+ sorted_tables.each do |table_name|
+ table(table_name, stream) unless ignored?(table_name)
+ end
+
+ # dump foreign keys at the end to make sure all dependent tables exist.
+ if @connection.supports_foreign_keys?
+ sorted_tables.each do |tbl|
+ foreign_keys(tbl, stream)
end
- table(tbl, stream)
end
end
@@ -112,7 +113,8 @@ HEADER
# first dump primary key column
if @connection.respond_to?(:pk_and_sequence_for)
pk, _ = @connection.pk_and_sequence_for(table)
- elsif @connection.respond_to?(:primary_key)
+ end
+ if !pk && @connection.respond_to?(:primary_key)
pk = @connection.primary_key(table)
end
@@ -212,8 +214,49 @@ HEADER
end
end
+ def foreign_keys(table, stream)
+ if (foreign_keys = @connection.foreign_keys(table)).any?
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
+ parts = [
+ 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect,
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
+
+ if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
+ parts << ('column: ' + foreign_key.column.inspect)
+ end
+
+ if foreign_key.custom_primary_key?
+ parts << ('primary_key: ' + foreign_key.primary_key.inspect)
+ end
+
+ if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ parts << ('name: ' + foreign_key.name.inspect)
+ end
+
+ parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update
+ parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete
+
+ ' ' + parts.join(', ')
+ end
+
+ stream.puts add_foreign_key_statements.sort.join("\n")
+ end
+ end
+
def remove_prefix_and_suffix(table)
table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
+
+ def ignored?(table_name)
+ ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ case ignored
+ when String; remove_prefix_and_suffix(table_name) == ignored
+ when Regexp; remove_prefix_and_suffix(table_name) =~ ignored
+ else
+ raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index a9d164e366..b5038104ac 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -5,6 +5,9 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
class << self
+ def primary_key
+ nil
+ end
def table_name
"#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
@@ -36,6 +39,14 @@ module ActiveRecord
connection.drop_table(table_name)
end
end
+
+ def normalize_migration_number(number)
+ "%.3d" % number.to_i
+ end
+
+ def normalized_versions
+ pluck(:version).map { |v| normalize_migration_number v }
+ end
end
def version
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 01fec31544..18190cb535 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -11,7 +11,7 @@ module ActiveRecord
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
@@ -19,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"
@@ -93,14 +94,14 @@ module ActiveRecord
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|
- default_scope.merge(unscoped { scope.call })
+ 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/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 1a766093d0..c2484d02ed 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -180,13 +180,9 @@ module ActiveRecord #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
- type = if klass.serialized_attributes.key?(name)
- super
- elsif klass.columns_hash.key?(name)
- klass.columns_hash[name].type
- else
- NilClass
- end
+ column = klass.columns_hash[name] || Type::Value.new
+
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
{ :text => :string,
:time => :datetime }[type] || type
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 79a6ccbda0..3c291f28e3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -66,8 +66,9 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :stored_attributes, instance_accessor: false
- self.stored_attributes = {}
+ class << self
+ attr_accessor :local_stored_attributes
+ end
end
module ClassMethods
@@ -93,18 +94,26 @@ module ActiveRecord
# 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
+ self.local_stored_attributes ||= {}
+ self.local_stored_attributes[store_attribute] ||= []
+ self.local_stored_attributes[store_attribute] |= keys
end
- def _store_accessors_module
+ def _store_accessors_module # :nodoc:
@_store_accessors_module ||= begin
mod = Module.new
include mod
mod
end
end
+
+ def stored_attributes
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
+ if self.local_stored_attributes
+ parent.merge!(self.local_stored_attributes) { |k, a, b| a | b }
+ end
+ parent
+ end
end
protected
@@ -120,10 +129,10 @@ module ActiveRecord
private
def store_accessor_for(store_attribute)
- @column_types[store_attribute.to_s].accessor
+ type_for_attribute(store_attribute.to_s).accessor
end
- class HashAccessor
+ class HashAccessor # :nodoc:
def self.read(object, attribute, key)
prepare(object, attribute)
object.public_send(attribute)[key]
@@ -142,7 +151,7 @@ module ActiveRecord
end
end
- class StringKeyedHashAccessor < HashAccessor
+ class StringKeyedHashAccessor < HashAccessor # :nodoc:
def self.read(object, attribute, key)
super object, attribute, key.to_s
end
@@ -152,7 +161,7 @@ module ActiveRecord
end
end
- class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc:
def self.prepare(object, store_attribute)
attribute = object.send(store_attribute)
unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 6ce0495f6f..892c78e479 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -6,7 +6,7 @@ module ActiveRecord
# <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates
# logic behind common tasks used to manage database and migrations.
#
- # The tasks defined here are used in rake tasks provided by Active Record.
+ # The tasks defined here are used with Rake tasks provided by Active Record.
#
# In order to use DatabaseTasks, a few config values need to be set. All the needed
# config values are set by Rails already, so it's necessary to do it only if you
@@ -14,7 +14,6 @@ module ActiveRecord
# (in such case after configuring the database tasks, you can also use the rake tasks
# defined in Active Record).
#
- #
# The possible config values are:
#
# * +env+: current environment (like Rails.env).
@@ -28,7 +27,7 @@ module ActiveRecord
# 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...
#
@@ -59,7 +58,11 @@ module ActiveRecord
end
def fixtures_path
- @fixtures_path ||= File.join(root, 'test', 'fixtures')
+ @fixtures_path ||= if ENV['FIXTURES_PATH']
+ File.join(root, ENV['FIXTURES_PATH'])
+ else
+ File.join(root, 'test', 'fixtures')
+ end
end
def root
@@ -107,6 +110,8 @@ module ActiveRecord
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).drop
+ rescue ActiveRecord::NoDatabaseError
+ $stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't drop #{configuration['database']}"
@@ -122,6 +127,16 @@ module ActiveRecord
}
end
+ def migrate
+ verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ scope = ENV['SCOPE']
+ Migration.verbose = verbose
+ Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ scope.blank? || scope == migration.scope
+ end
+ end
+
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -144,6 +159,18 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(configuration).purge
end
+ def purge_all
+ each_local_configuration { |configuration|
+ purge configuration
+ }
+ end
+
+ def purge_current(environment = env)
+ each_current_configuration(environment) { |configuration|
+ purge configuration
+ }
+ end
+
def structure_dump(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
@@ -157,20 +184,39 @@ module ActiveRecord
end
def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
+ This method will act on a specific connection in the future.
+ To act on the current connection, use `load_schema_current` instead.
+ MESSAGE
+ load_schema_current(format, file)
+ end
+
+ # This method is the successor of +load_schema+. We should rename it
+ # after +load_schema+ went through a deprecation cycle. (Rails > 4.2)
+ def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc:
case format
when :ruby
file ||= File.join(db_dir, "schema.rb")
check_schema_file(file)
+ purge(configuration)
+ ActiveRecord::Base.establish_connection(configuration)
load(file)
when :sql
file ||= File.join(db_dir, "structure.sql")
check_schema_file(file)
- structure_load(current_config, file)
+ purge(configuration)
+ structure_load(configuration, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
end
+ def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
+ each_current_configuration(environment) { |configuration|
+ load_schema_for configuration, format, file
+ }
+ end
+
def check_schema_file(filename)
unless File.exist?(filename)
message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index c755831e6d..d890196f47 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -42,7 +42,7 @@ module ActiveRecord
end
def purge
- establish_connection :test
+ establish_connection configuration
connection.recreate_database configuration['database'], creation_options
end
@@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
end
def root_password
- $stdout.print "Please provide the root password for your mysql installation\n>"
+ $stdout.print "Please provide the root password for your MySQL installation\n>"
$stdin.gets.strip
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 3d02ee07d0..ce1de4b76e 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -54,7 +54,7 @@ module ActiveRecord
command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
raise 'Error dumping database' unless Kernel.system(command)
- File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" }
+ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
def structure_load(filename)
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index 5688931db2..9ab64d0325 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -21,7 +21,11 @@ module ActiveRecord
FileUtils.rm(file) if File.exist?(file)
end
- alias :purge :drop
+
+ def purge
+ drop
+ create
+ end
def charset
connection.encoding
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 271f58cbe0..5ef98ed820 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Timestamp
#
@@ -43,12 +42,12 @@ module ActiveRecord
private
- def create_record
+ def _create_record
if self.record_timestamps
current_time = current_time_from_proper_timezone
all_timestamp_attributes.each do |column|
- if attributes.has_key?(column.to_s) && self.send(column).nil?
+ if attributes.key?(column.to_s) && self.send(column).nil?
write_attribute(column.to_s, current_time)
end
end
@@ -57,7 +56,7 @@ module ActiveRecord
super
end
- def update_record(*args)
+ def _update_record(*args)
if should_record_timestamps?
current_time = current_time_from_proper_timezone
@@ -99,9 +98,11 @@ module ActiveRecord
end
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
+ timestamp_names
+ .map { |attr| self[attr] }
+ .compact
+ .map(&:to_time)
+ .max
end
def current_time_from_proper_timezone
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index ec3e8f281b..7e4dc4c895 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -1,5 +1,3 @@
-require 'thread'
-
module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
@@ -295,7 +293,7 @@ module ActiveRecord
def committed! #:nodoc:
run_callbacks :commit if destroyed? || persisted?
ensure
- @_start_transaction_state.clear
+ force_clear_transaction_record_state
end
# Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
@@ -328,7 +326,7 @@ module ActiveRecord
begin
status = yield
rescue ActiveRecord::Rollback
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ clear_transaction_record_state
status = nil
end
@@ -341,7 +339,7 @@ module ActiveRecord
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc:
- @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
+ @_start_transaction_state[:id] = id
unless @_start_transaction_state.include?(:new_record)
@_start_transaction_state[:new_record] = @new_record
end
@@ -349,13 +347,18 @@ module ActiveRecord
@_start_transaction_state[:destroyed] = @destroyed
end
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
- @_start_transaction_state[:frozen?] = @attributes.frozen?
+ @_start_transaction_state[:frozen?] = frozen?
end
# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc:
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- @_start_transaction_state.clear if @_start_transaction_state[:level] < 1
+ force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
+ end
+
+ # Force to clear the transaction record state.
+ def force_clear_transaction_record_state #:nodoc:
+ @_start_transaction_state.clear
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
@@ -364,17 +367,10 @@ module ActiveRecord
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?
+ thaw unless restore_state[:frozen?]
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- if restore_state.has_key?(:id)
- self.id = restore_state[:id]
- else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
- end
- @attributes.freeze if was_frozen
+ write_attribute(self.class.primary_key, restore_state[:id])
end
end
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
new file mode 100644
index 0000000000..f1384e0bb2
--- /dev/null
+++ b/activerecord/lib/active_record/type.rb
@@ -0,0 +1,20 @@
+require 'active_record/type/mutable'
+require 'active_record/type/numeric'
+require 'active_record/type/time_value'
+require 'active_record/type/value'
+
+require 'active_record/type/binary'
+require 'active_record/type/boolean'
+require 'active_record/type/date'
+require 'active_record/type/date_time'
+require 'active_record/type/decimal'
+require 'active_record/type/decimal_without_scale'
+require 'active_record/type/float'
+require 'active_record/type/integer'
+require 'active_record/type/serialized'
+require 'active_record/type/string'
+require 'active_record/type/text'
+require 'active_record/type/time'
+
+require 'active_record/type/type_map'
+require 'active_record/type/hash_lookup_type_map'
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
new file mode 100644
index 0000000000..d29ff4e494
--- /dev/null
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module Type
+ class Binary < Value # :nodoc:
+ def type
+ :binary
+ end
+
+ def binary?
+ true
+ end
+
+ def type_cast(value)
+ if value.is_a?(Data)
+ value.to_s
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value.to_s
+ end
+
+ def to_s
+ @value
+ end
+
+ def hex
+ @value.unpack('H*')[0]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
new file mode 100644
index 0000000000..06dd17ed28
--- /dev/null
+++ b/activerecord/lib/active_record/type/boolean.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class Boolean < Value # :nodoc:
+ def type
+ :boolean
+ end
+
+ private
+
+ def cast_value(value)
+ if value == ''
+ nil
+ else
+ ConnectionAdapters::Column::TRUE_VALUES.include?(value)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
new file mode 100644
index 0000000000..d90a6069b7
--- /dev/null
+++ b/activerecord/lib/active_record/type/date.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module Type
+ class Date < Value # :nodoc:
+ def type
+ :date
+ end
+
+ def klass
+ ::Date
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ def fast_string_to_date(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
new file mode 100644
index 0000000000..5f19608a33
--- /dev/null
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module Type
+ class DateTime < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :datetime
+ end
+
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
+ private
+
+ def cast_value(string)
+ return string unless string.is_a?(::String)
+ return if string.empty?
+
+ fast_string_to_time(string) || fallback_string_to_time(string)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ 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, :offset))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
new file mode 100644
index 0000000000..d10778eeb6
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module Type
+ class Decimal < Value # :nodoc:
+ include Numeric
+
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when ::Float
+ BigDecimal(value, float_precision)
+ when ::Numeric, ::String
+ BigDecimal(value, precision.to_i)
+ else
+ if value.respond_to?(:to_d)
+ value.to_d
+ else
+ cast_value(value.to_s)
+ end
+ end
+ end
+
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..cabdcecdd7
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/integer'
+
+module ActiveRecord
+ module Type
+ class DecimalWithoutScale < Integer # :nodoc:
+ def type
+ :decimal
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
new file mode 100644
index 0000000000..42eb44b9a9
--- /dev/null
+++ b/activerecord/lib/active_record/type/float.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class Float < Value # :nodoc:
+ include Numeric
+
+ def type
+ :float
+ end
+
+ alias type_cast_for_database type_cast
+
+ private
+
+ def cast_value(value)
+ value.to_f
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
new file mode 100644
index 0000000000..bf92680268
--- /dev/null
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class HashLookupTypeMap < TypeMap # :nodoc:
+ delegate :key?, to: :@mapping
+
+ def lookup(type, *args)
+ @mapping.fetch(type, proc { default_value }).call(type, *args)
+ end
+
+ def fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
+ end
+
+ def alias_type(type, alias_type)
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
new file mode 100644
index 0000000000..08477d1303
--- /dev/null
+++ b/activerecord/lib/active_record/type/integer.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module Type
+ class Integer < Value # :nodoc:
+ include Numeric
+
+ def type
+ :integer
+ end
+
+ alias type_cast_for_database type_cast
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else value.to_i rescue nil
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
new file mode 100644
index 0000000000..066617ea59
--- /dev/null
+++ b/activerecord/lib/active_record/type/mutable.rb
@@ -0,0 +1,16 @@
+module ActiveRecord
+ module Type
+ module Mutable # :nodoc:
+ def type_cast_from_user(value)
+ type_cast_from_database(type_cast_for_database(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != type_cast_for_database(new_value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
new file mode 100644
index 0000000000..fa43266504
--- /dev/null
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ module Numeric # :nodoc:
+ def number?
+ true
+ end
+
+ def type_cast(value)
+ value = case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
+ super(value)
+ end
+
+ def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
+ end
+
+ private
+
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
+ def non_numeric_string?(value)
+ # 'wibble'.to_i will give zero, we want to make sure
+ # that we aren't marking int zero to string zero as
+ # changed.
+ value.to_s !~ /\A\d+\.?\d*\z/
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
new file mode 100644
index 0000000000..abeea769c4
--- /dev/null
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -0,0 +1,51 @@
+module ActiveRecord
+ module Type
+ class Serialized < SimpleDelegator # :nodoc:
+ include Mutable
+
+ attr_reader :subtype, :coder
+
+ def initialize(subtype, coder)
+ @subtype = subtype
+ @coder = coder
+ super(subtype)
+ end
+
+ def type_cast_from_database(value)
+ if default_value?(value)
+ value
+ else
+ coder.load(super)
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ unless default_value?(value)
+ super coder.dump(value)
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
+
+ def init_with(coder)
+ @subtype = coder['subtype']
+ @coder = coder['coder']
+ __setobj__(@subtype)
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = @subtype
+ coder['coder'] = @coder
+ end
+
+ private
+
+ def default_value?(value)
+ value == coder.load(nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
new file mode 100644
index 0000000000..150defb106
--- /dev/null
+++ b/activerecord/lib/active_record/type/string.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ class String < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def type_cast_for_database(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "1"
+ when false then "0"
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
new file mode 100644
index 0000000000..26f980f060
--- /dev/null
+++ b/activerecord/lib/active_record/type/text.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/string'
+
+module ActiveRecord
+ module Type
+ class Text < String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
new file mode 100644
index 0000000000..41f7d97f0c
--- /dev/null
+++ b/activerecord/lib/active_record/type/time.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module Type
+ class Time < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :time
+ end
+
+ private
+
+ def cast_value(value)
+ return value unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = "2000-01-01 #{value}"
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb
new file mode 100644
index 0000000000..d611d72dd4
--- /dev/null
+++ b/activerecord/lib/active_record/type/time_value.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ module Type
+ module TimeValue # :nodoc:
+ def klass
+ ::Time
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return 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
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
+ microsec = ($7.to_r * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
new file mode 100644
index 0000000000..88c5f9c497
--- /dev/null
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -0,0 +1,48 @@
+module ActiveRecord
+ module Type
+ class TypeMap # :nodoc:
+ def initialize
+ @mapping = {}
+ end
+
+ def lookup(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
+
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ default_value
+ end
+ end
+
+ def register_type(key, value = nil, &block)
+ raise ::ArgumentError unless value || block
+
+ if block
+ @mapping[key] = block
+ else
+ @mapping[key] = proc { value }
+ end
+ end
+
+ def alias_type(key, target_key)
+ register_type(key) do |sql_type, *args|
+ metadata = sql_type[/\(.*\)/, 0]
+ lookup("#{target_key}#{metadata}", *args)
+ end
+ end
+
+ def clear
+ @mapping.clear
+ end
+
+ private
+
+ def default_value
+ @default_value ||= Value.new
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
new file mode 100644
index 0000000000..475e130013
--- /dev/null
+++ b/activerecord/lib/active_record/type/value.rb
@@ -0,0 +1,94 @@
+module ActiveRecord
+ module Type
+ class Value # :nodoc:
+ attr_reader :precision, :scale, :limit
+
+ # Valid options are +precision+, +scale+, and +limit+. They are only
+ # used when dumping schema.
+ def initialize(options = {})
+ options.assert_valid_keys(:precision, :scale, :limit)
+ @precision = options[:precision]
+ @scale = options[:scale]
+ @limit = options[:limit]
+ end
+
+ # The simplified type that this object represents. Returns a symbol such
+ # as +:string+ or +:integer+
+ def type; end
+
+ # Type casts a string from the database into the appropriate ruby type.
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +cast_value+ instead.
+ def type_cast_from_database(value)
+ type_cast(value)
+ end
+
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or an already type cast value
+ # provided manually to a setter.
+ #
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +type_cast+ or +cast_value+
+ # instead.
+ def type_cast_from_user(value)
+ type_cast(value)
+ end
+
+ # Cast a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+
+ def type_cast_for_database(value)
+ value
+ end
+
+ # Type cast a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
+ value.inspect
+ end
+
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def number? # :nodoc:
+ false
+ end
+
+ def binary? # :nodoc:
+ false
+ end
+
+ def klass # :nodoc:
+ end
+
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
+ old_value != new_value
+ end
+
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. This method should not be overridden
+ # directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
+ false
+ end
+
+ private
+
+ def type_cast(value)
+ cast_value(value) unless value.nil?
+ end
+
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by
+ # `type_cast_from_database` and `type_cast_from_user` for all values
+ # except `nil`.
+ def cast_value(value) # :doc:
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 26dca415ff..7f7d49cdb4 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -29,21 +29,6 @@ module ActiveRecord
extend ActiveSupport::Concern
include ActiveModel::Validations
- module ClassMethods
- # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
- # so an exception is raised if the record is invalid.
- def create!(attributes = nil, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, &block) }
- else
- object = new(attributes)
- yield(object) if block_given?
- object.save!
- object
- end
- end
- end
-
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
# The regular Base#save method is replaced with this when the validations
# module is mixed in, which it is by default.
@@ -54,12 +39,14 @@ module ActiveRecord
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
# exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise(RecordInvalid.new(self))
+ perform_validations(options) ? super : raise_record_invalid
end
# 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,8 +58,26 @@ module ActiveRecord
errors.empty? && output
end
+ alias_method :validate, :valid?
+
+ # Runs all the validations within the specified context. Returns +true+ if
+ # no errors are found, raises +RecordInvalid+ otherwise.
+ #
+ # 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.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
+ def validate!(context = nil)
+ valid?(context) || raise_record_invalid
+ end
+
protected
+ def raise_record_invalid
+ raise(RecordInvalid.new(self))
+ end
+
def perform_validations(options={}) # :nodoc:
options[:validate] == false || valid?(options[:context])
end
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 9a19483da3..e586744818 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -4,7 +4,7 @@ module ActiveRecord
def validate(record)
super
attributes.each do |attribute|
- next unless record.class.reflect_on_association(attribute)
+ next unless record.class._reflect_on_association(attribute)
associated_records = Array.wrap(record.send(attribute))
# Superclass validates presence. Ensure present records aren't about to be destroyed.
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 7ebe9dfec0..2dba4c7b94 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -13,7 +13,7 @@ module ActiveRecord
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
- value = deserialize_attribute(record, attribute, value)
+ value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted?
@@ -46,9 +46,9 @@ module ActiveRecord
end
def build_relation(klass, table, attribute, value) #:nodoc:
- if reflection = klass.reflect_on_association(attribute)
+ if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name] unless value.nil?
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
attribute_name = attribute.to_s
@@ -61,20 +61,21 @@ module ActiveRecord
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?
+ if value.is_a?(String) && column.limit
+ value = value.to_s[0, column.limit]
+ end
- if !options[:case_sensitive] && value && column.text?
+ if !options[:case_sensitive] && value.is_a?(String)
# 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
def scope_relation(record, table, relation)
Array(options[:scope]).each do |scope_item|
- if reflection = record.class.reflect_on_association(scope_item)
+ if reflection = record.class._reflect_on_association(scope_item)
scope_value = record.send(reflection.foreign_key)
scope_item = reflection.foreign_key
else
@@ -86,9 +87,9 @@ module ActiveRecord
relation
end
- def deserialize_attribute(record, attribute, value)
- coder = record.class.serialized_attributes[attribute.to_s]
- value = coder.dump value if value && coder
+ def map_enum_attribute(klass, attribute, value)
+ mapping = klass.defined_enums[attribute.to_s]
+ value = mapping[value] if value && mapping
value
end
end
@@ -151,7 +152,7 @@ module ActiveRecord
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
# proc or string should return or evaluate to a +true+ or +false+ value.
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
- # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>,
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a +true+ or +false+
# value.
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 863c3ebe4d..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.beta1"
- 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/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/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 59324c4857..64cde143a1 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -29,6 +29,7 @@ module ActiveRecord
@columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
options[:default],
+ lookup_cast_type(sql_type.to_s),
sql_type.to_s,
options[:null])
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 0eb1231c79..6f84bae432 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -46,9 +46,7 @@ module ActiveRecord
@connection.add_index :accounts, :firm_id, :name => idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
- # OpenBase does not have the concept of a named index
- # Indexes are merely properties of columns.
- assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
+ assert_equal idx_name, indexes.first.name
assert !indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
else
@@ -127,14 +125,12 @@ module ActiveRecord
assert_equal 1, Movie.create(:name => 'fight club').id
end
- if ActiveRecord::Base.connection.adapter_name != "FrontBase"
- def test_reset_table_with_non_integer_pk
- Subscriber.delete_all
- Subscriber.connection.reset_pk_sequence! 'subscribers'
- sub = Subscriber.new(:name => 'robert drake')
- sub.id = 'bob drake'
- assert_nothing_raised { sub.save! }
- end
+ def test_reset_table_with_non_integer_pk
+ Subscriber.delete_all
+ Subscriber.connection.reset_pk_sequence! 'subscribers'
+ sub = Subscriber.new(:name => 'robert drake')
+ sub.id = 'bob drake'
+ assert_nothing_raised { sub.save! }
end
end
@@ -145,8 +141,8 @@ module ActiveRecord
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?
@@ -157,6 +153,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
@@ -184,7 +192,7 @@ module ActiveRecord
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)
+ query = author.posts.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)
@@ -218,7 +226,7 @@ module ActiveRecord
@connection = Klass.connection
end
- def teardown
+ teardown do
Klass.remove_connection
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 5cd5d8ac5f..a7b0addc1b 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
@@ -69,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) 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) 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) 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
@@ -138,12 +134,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert_equal [["STRICT_ALL_TABLES"]], result.rows
end
- def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
+ def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
+ result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal [['']], result.rows
end
end
@@ -155,6 +150,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overrides_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}}))
@@ -166,12 +169,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/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb
new file mode 100644
index 0000000000..083d533bb2
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb
@@ -0,0 +1,48 @@
+require "cases/helper"
+
+class MysqlConsistencyTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Consistency < ActiveRecord::Base
+ self.table_name = "mysql_consistency"
+ end
+
+ setup do
+ @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
+
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("mysql_consistency") do |t|
+ t.boolean "a_bool"
+ t.string "a_string"
+ end
+ Consistency.reset_column_information
+ Consistency.create!
+ end
+
+ teardown do
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans
+ @connection.drop_table "mysql_consistency"
+ end
+
+ test "boolean columns with random value type cast to 0 when emulate_booleans is false" do
+ with_new = Consistency.new
+ with_last = Consistency.last
+ with_new.a_bool = 'wibble'
+ with_last.a_bool = 'wibble'
+
+ assert_equal 0, with_new.a_bool
+ assert_equal 0, with_last.a_bool
+ end
+
+ test "string columns call #to_s" do
+ with_new = Consistency.new
+ with_last = Consistency.last
+ thing = Object.new
+ with_new.a_string = thing
+ with_last.a_string = thing
+
+ assert_equal thing.to_s, with_new.a_string
+ assert_equal thing.to_s, with_last.a_string
+ 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 578f6301bd..28106d3772 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,19 +1,15 @@
# 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) auto_increment PRIMARY KEY,
- `number` integer,
- `data` varchar(255))
- eosql
end
def test_bad_connection_mysql
@@ -25,8 +21,10 @@ module ActiveRecord
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
@@ -38,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
@@ -74,46 +76,37 @@ 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) 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) 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_from_database(result.last['status'])
+ end
end
def test_supports_extensions
@@ -140,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/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb
index 3d1330efb8..d8a954efa8 100644
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb
@@ -9,13 +9,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'boolean')
+ c = Column.new(nil, 1, Type::Boolean.new)
assert_equal 1, @conn.type_cast(true, nil)
assert_equal 1, @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'boolean')
+ c = Column.new(nil, 1, Type::Boolean.new)
assert_equal 0, @conn.type_cast(false, nil)
assert_equal 0, @conn.type_cast(false, c)
end
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 8eb9565963..61ae0abfd1 100644
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
@@ -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/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index 807a7a155e..87c5277e64 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -17,6 +17,44 @@ module ActiveRecord
self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
+
+ @connection.create_table "mysql_doubles"
+ end
+
+ teardown do
+ @connection.execute "drop table if exists mysql_doubles"
+ end
+
+ class MysqlDouble < ActiveRecord::Base
+ self.table_name = "mysql_doubles"
+ end
+
+ def test_float_limits
+ @connection.add_column :mysql_doubles, :float_no_limit, :float
+ @connection.add_column :mysql_doubles, :float_short, :float, limit: 5
+ @connection.add_column :mysql_doubles, :float_long, :float, limit: 53
+
+ @connection.add_column :mysql_doubles, :float_23, :float, limit: 23
+ @connection.add_column :mysql_doubles, :float_24, :float, limit: 24
+ @connection.add_column :mysql_doubles, :float_25, :float, limit: 25
+ MysqlDouble.reset_column_information
+
+ column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' }
+ column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' }
+ column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' }
+
+ column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' }
+ column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' }
+ column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' }
+
+ # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ assert_equal 24, column_no_limit.limit
+ assert_equal 24, column_short.limit
+ assert_equal 53, column_long.limit
+
+ assert_equal 24, column_23.limit
+ assert_equal 24, column_24.limit
+ assert_equal 53, column_25.limit
end
def test_schema
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
index 267aa232d9..f3c711a64b 100644
--- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal "1", attributes["published"]
assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal 1, @connection.type_cast(true, string_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
end
test "test type casting without emulated booleans" do
@@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase
assert_equal "1", attributes["published"]
assert_equal 1, @connection.type_cast(true, boolean_column)
- assert_equal 1, @connection.type_cast(true, string_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
end
test "with booleans stored as 1 and 0" do
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 9b7202c915..beedb4f3a1 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -1,15 +1,18 @@
require "cases/helper"
+require 'support/connection_helper'
class MysqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
def setup
super
@subscriber = SQLSubscriber.new
- ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
end
def teardown
- ActiveSupport::Notifications.unsubscribe(@subscriber)
+ ActiveSupport::Notifications.unsubscribe(@subscription)
super
end
@@ -57,12 +60,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert_equal [["STRICT_ALL_TABLES"]], result.rows
end
- def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
+ def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
+ result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal [['']], result.rows
end
end
@@ -74,6 +76,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overrides_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}}))
@@ -97,14 +107,10 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@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 1cd356e868..675703caa1 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/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` = 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 %(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 %(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
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index 1a82308176..799d927ee4 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -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 ec73ec35aa..9c49599d34 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -6,12 +6,12 @@ module ActiveRecord
class SchemaMigrationsTest < ActiveRecord::TestCase
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.add_foreign_key :engines, :cars, name: "fk_engines_cars"
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"
+ connection.remove_foreign_key :engines, name: "fk_engines_cars"
end
def test_initializes_schema_migrations_for_encoding_utf8mb4
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 3090f4478f..8df1b7d18c 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -1,32 +1,45 @@
# encoding: utf-8
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlArrayTest < ActiveRecord::TestCase
+ include InTimeZone
+ OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
+
class PgArray < ActiveRecord::Base
self.table_name = 'pg_arrays'
end
def setup
@connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
+
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
t.string 'tags', array: true
t.integer 'ratings', array: true
+ t.datetime :datetimes, array: true
+ t.hstore :hstores, array: true
end
end
- @column = PgArray.columns.find { |c| c.name == 'tags' }
+ @column = PgArray.columns_hash['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
@@ -34,15 +47,35 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_not ratings_column.number?
end
+ def test_default
+ @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
+ PgArray.reset_column_information
+
+ assert_equal([4, 4, 2], PgArray.column_defaults['score'])
+ 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
+
+ assert_equal(["foo", "bar"], PgArray.column_defaults['names'])
+ 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: "{}"
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: []
PgArray.reset_column_information
- column = PgArray.columns.find { |c| c.name == 'snippets' }
+ column = PgArray.columns_hash['snippets']
assert_equal :text, column.type
- assert_equal [], column.default
+ assert_equal [], PgArray.column_defaults['snippets']
assert column.array
end
@@ -55,38 +88,56 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
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
- # normal use of string_to_array, it's called from the OID::Array
- # class and will have the OID instance that will provide the type
- # casting
- array = @column.class.string_to_array data, oid_type
- assert_equal(['1', '2', '3'], array)
- assert_equal(['1', '2', '3'], @column.type_cast(data))
+ def test_change_column_default_with_array
+ @connection.change_column_default :pg_arrays, :tags, []
- assert_equal([], @column.type_cast('{}'))
- assert_equal([nil], @column.type_cast('{NULL}'))
+ PgArray.reset_column_information
+ assert_equal [], PgArray.column_defaults['tags']
+ end
+
+ def test_type_cast_array
+ assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}'))
+ assert_equal([], @column.type_cast_from_database('{}'))
+ assert_equal([nil], @column.type_cast_from_database('{NULL}'))
end
def test_type_cast_integers
x = PgArray.new(ratings: ['1', '2'])
- assert x.save!
- assert_equal(['1', '2'], x.ratings)
+
+ assert_equal([1, 2], x.ratings)
+
+ x.save!
+ x.reload
+
+ assert_equal([1, 2], x.ratings)
end
- def test_rewrite
+ def test_select_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- x.tags = ['1','2','3','4']
- assert x.save!
+ assert_equal(['1','2','3'], x.tags)
end
- def test_select
+ def test_rewrite_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- assert_equal(['1','2','3'], x.tags)
+ x.tags = ['1','2','3','4']
+ x.save!
+ assert_equal ['1','2','3','4'], x.reload.tags
+ end
+
+ def test_select_with_integers
+ @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
+ x = PgArray.first
+ assert_equal([1, 2, 3], x.ratings)
+ end
+
+ def test_rewrite_with_integers
+ @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
+ x = PgArray.first
+ x.ratings = [2, '3', 4]
+ x.save!
+ assert_equal [2, 3, 4], x.reload.ratings
end
def test_multi_dimensional_with_strings
@@ -140,14 +191,71 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
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"]
+ def test_escaping
+ unknown = 'foo\\",bar,baz,\\'
+ tags = ["hello_#{unknown}"]
+ ar = PgArray.create!(tags: tags)
+ ar.reload
+ assert_equal tags, ar.tags
+ end
+
+ def test_string_quoting_rules_match_pg_behavior
+ tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"]
+ x = PgArray.create!(tags: tags)
+ x.reload
+
+ assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ end
+
+ def test_quoting_non_standard_delimiters
+ strings = ["hello,", "world;"]
+ comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',')
+ semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';')
+
+ assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings)
+ assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings)
+ end
+
+ def test_mutate_array
+ x = PgArray.create!(tags: %w(one two))
- PgArray.update_all tags: ["four", "five"]
- assert_equal ["four", "five"], pg_array.reload.tags
+ x.tags << "three"
+ x.save!
+ x.reload
+
+ assert_equal %w(one two three), x.tags
+ assert_not x.changed?
+ end
+
+ def test_mutate_value_in_array
+ x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }])
+
+ x.hstores.first['a'] = 'c'
+ x.save!
+ x.reload
- PgArray.update_all tags: []
- assert_equal [], pg_array.reload.tags
+ assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores
+ assert_not x.changed?
+ end
+
+ def test_datetime_with_timezone_awareness
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PgArray.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
+
+ record = PgArray.new(datetimes: [time_string])
+ assert_equal [time], record.datetimes
+ assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
+
+ record.save!
+ record.reload
+
+ assert_equal [time], record.datetimes
+ assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
+ end
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
new file mode 100644
index 0000000000..72222c01fd
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlBitStringTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include SchemaDumpingHelper
+
+ class PostgresqlBitString < ActiveRecord::Base; end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_bit_strings', :force => true) do |t|
+ t.bit :a_bit, default: "00000011", limit: 8
+ t.bit_varying :a_bit_varying, default: "0011", limit: 4
+ end
+ end
+
+ def teardown
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings'
+ end
+
+ def test_bit_string_column
+ column = PostgresqlBitString.columns_hash["a_bit"]
+ assert_equal :bit, column.type
+ assert_equal "bit(8)", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_bit_string_varying_column
+ column = PostgresqlBitString.columns_hash["a_bit_varying"]
+ assert_equal :bit_varying, column.type
+ assert_equal "bit varying(4)", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit']
+ assert_equal "00000011", PostgresqlBitString.new.a_bit
+
+ assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying']
+ assert_equal "0011", PostgresqlBitString.new.a_bit_varying
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_bit_strings")
+ assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output
+ assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output
+ end
+
+ def test_assigning_invalid_hex_string_raises_exception
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" }
+ end
+
+ def test_roundtrip
+ PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101"
+ record = PostgresqlBitString.first
+ assert_equal "00001010", record.a_bit
+ assert_equal "0101", record.a_bit_varying
+
+ record.a_bit = "11111111"
+ record.a_bit_varying = "0xF"
+ record.save!
+
+ assert record.reload
+ assert_equal "11111111", record.a_bit
+ assert_equal "1111", record.a_bit_varying
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index b8dd35c4c5..7872f91943 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,8 +1,5 @@
# encoding: utf-8
-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlByteaTest < ActiveRecord::TestCase
class ByteaDataType < ActiveRecord::Base
@@ -19,11 +16,11 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
end
end
end
- @column = ByteaDataType.columns.find { |c| c.name == 'payload' }
+ @column = ByteaDataType.columns_hash['payload']
assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn))
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists bytea_data_type'
end
@@ -36,16 +33,16 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
data = "\u001F\x8B"
assert_equal('UTF-8', data.encoding.name)
- assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name)
+ assert_equal('ASCII-8BIT', @column.type_cast_from_database(data).encoding.name)
end
def test_type_cast_binary_value
data = "\u001F\x8B".force_encoding("BINARY")
- assert_equal(data, @column.type_cast(data))
+ assert_equal(data, @column.type_cast_from_database(data))
end
def test_type_case_nil
- assert_equal(nil, @column.type_cast(nil))
+ assert_equal(nil, @column.type_cast_from_database(nil))
end
def test_read_value
@@ -70,6 +67,23 @@ class PostgresqlByteaTest < ActiveRecord::TestCase
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)
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..2acb64f81c
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -0,0 +1,76 @@
+# encoding: utf-8
+require 'cases/helper'
+
+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.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_hash['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
index 7202cce390..cfab5ca902 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/connection_helper'
+
+module PostgresqlCompositeBehavior
+ include ConnectionHelper
-class PostgresqlCompositeTest < ActiveRecord::TestCase
class PostgresqlComposite < ActiveRecord::Base
self.table_name = "postgresql_composites"
end
- def teardown
- @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
- @connection.execute 'DROP TYPE IF EXISTS full_address'
- end
-
def setup
+ super
+
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.execute <<-SQL
@@ -29,7 +27,37 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
end
end
+ def teardown
+ super
+
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
+ @connection.execute 'DROP TYPE IF EXISTS full_address'
+ reset_connection
+ PostgresqlComposite.reset_column_information
+ end
+end
+
+# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like:
+# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String."
+# To take full advantage of composite types, we suggest you register your own +OID::Type+.
+# See PostgresqlCompositeWithCustomOIDTest
+class PostgresqlCompositeTest < ActiveRecord::TestCase
+ include PostgresqlCompositeBehavior
+
+ def test_column
+ ensure_warning_is_issued
+
+ column = PostgresqlComposite.columns_hash["address"]
+ assert_nil column.type
+ assert_equal "full_address", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
def test_composite_mapping
+ ensure_warning_is_issued
+
@connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
composite = PostgresqlComposite.first
assert_equal "(Paris,Champs-Élysées)", composite.address
@@ -39,4 +67,65 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase
assert_equal '(Paris,"Rue Basse")', composite.reload.address
end
+
+ private
+ def ensure_warning_is_issued
+ warning = capture(:stderr) do
+ PostgresqlComposite.columns_hash
+ end
+ assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning)
+ end
+end
+
+class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
+ include PostgresqlCompositeBehavior
+
+ class FullAddressType < ActiveRecord::Type::Value
+ def type; :full_address end
+
+ def type_cast_from_database(value)
+ if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
+ FullAddress.new($1, $2)
+ end
+ end
+
+ def type_cast_from_user(value)
+ value
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ "(#{value.city},#{value.street})"
+ end
+ end
+
+ FullAddress = Struct.new(:city, :street)
+
+ def setup
+ super
+
+ @connection.type_map.register_type "full_address", FullAddressType.new
+ end
+
+ def test_column
+ column = PostgresqlComposite.columns_hash["address"]
+ assert_equal :full_address, column.type
+ assert_equal "full_address", column.sql_type
+ assert_not column.number?
+ 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", composite.address.city
+ assert_equal "Champs-Élysées", composite.address.street
+
+ composite.address = FullAddress.new("Paris", "Rue Basse")
+ composite.save!
+
+ assert_equal 'Paris', composite.reload.address.city
+ assert_equal 'Rue Basse', composite.reload.address.street
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 4715fa002d..d26cda46fa 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -1,19 +1,22 @@
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
- ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
end
def teardown
- ActiveSupport::Notifications.unsubscribe(@subscriber)
+ ActiveSupport::Notifications.unsubscribe(@subscription)
super
end
@@ -45,6 +48,37 @@ 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', @subscriber.logged[0][1]
@@ -87,7 +121,7 @@ module ActiveRecord
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
+ plan = res.column_types['QUERY PLAN'].type_cast_from_database res.rows.first.first
assert_operator plan.length, :>, 0
end
@@ -167,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 04a458fbce..a0a34e4b87 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -1,13 +1,6 @@
require "cases/helper"
+require 'support/ddl_helper'
-class PostgresqlArray < ActiveRecord::Base
-end
-
-class PostgresqlTsvector < ActiveRecord::Base
-end
-
-class PostgresqlMoney < ActiveRecord::Base
-end
class PostgresqlNumber < ActiveRecord::Base
end
@@ -15,21 +8,9 @@ end
class PostgresqlTime < ActiveRecord::Base
end
-class PostgresqlNetworkAddress < ActiveRecord::Base
-end
-
-class PostgresqlBitString < ActiveRecord::Base
-end
-
class PostgresqlOid < ActiveRecord::Base
end
-class PostgresqlTimestampWithZone < ActiveRecord::Base
-end
-
-class PostgresqlUUID < ActiveRecord::Base
-end
-
class PostgresqlLtree < ActiveRecord::Base
end
@@ -38,57 +19,23 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.execute("set lc_monetary = 'C'")
-
- @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("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')")
-
- @first_tsvector = PostgresqlTsvector.find(1)
-
- @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)")
- @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)")
- @first_money = PostgresqlMoney.find(1)
- @second_money = PostgresqlMoney.find(2)
@connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')")
@first_number = PostgresqlNumber.find(1)
+ @second_number = PostgresqlNumber.find(2)
+ @third_number = PostgresqlNumber.find(3)
@connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')")
@first_time = PostgresqlTime.find(1)
- @connection.execute("INSERT INTO postgresql_network_addresses (id, cidr_address, inet_address, mac_address) VALUES(1, '192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')")
- @first_network_address = PostgresqlNetworkAddress.find(1)
-
- @connection.execute("INSERT INTO postgresql_bit_strings (id, bit_string, bit_string_varying) VALUES (1, B'00010101', X'15')")
- @first_bit_string = PostgresqlBitString.find(1)
-
@connection.execute("INSERT INTO postgresql_oids (id, obj_id) VALUES (1, 1234)")
@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
- [PostgresqlArray, PostgresqlTsvector, PostgresqlMoney, PostgresqlNumber, PostgresqlTime, PostgresqlNetworkAddress,
- PostgresqlBitString, PostgresqlOid, PostgresqlTimestampWithZone, PostgresqlUUID].each(&:delete_all)
- end
-
- def test_data_type_of_array_types
- assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type
- assert_equal :text, @first_array.column_for_attribute(:nicknames).type
- end
-
- def test_data_type_of_tsvector_types
- assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
end
- def test_data_type_of_money_types
- assert_equal :decimal, @first_money.column_for_attribute(:wealth).type
+ teardown do
+ [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all)
end
def test_data_type_of_number_types
@@ -101,61 +48,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type
end
- def test_data_type_of_network_address_types
- assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type
- assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type
- assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type
- end
-
- def test_data_type_of_bit_string_types
- assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type
- assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type
- end
-
def test_data_type_of_oid_types
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
- end
-
- def test_tsvector_values
- assert_equal "'text' 'vector'", @first_tsvector.text_vector
- end
-
- def test_money_values
- assert_equal 567.89, @first_money.wealth
- assert_equal(-567.89, @second_money.wealth)
- end
-
- 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'"
- @first_tsvector.text_vector = new_text_vector
- assert @first_tsvector.save
- assert @first_tsvector.reload
- @first_tsvector.text_vector = new_text_vector
- assert @first_tsvector.save
- assert @first_tsvector.reload
- assert_equal new_text_vector, @first_tsvector.text_vector
- end
-
def test_number_values
assert_equal 123.456, @first_number.single
assert_equal 123456.789, @first_number.double
+ assert_equal(-::Float::INFINITY, @second_number.single)
+ assert_equal ::Float::INFINITY, @second_number.double
+ assert_same ::Float::NAN, @third_number.double
end
def test_time_values
@@ -163,61 +65,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal '-21 days', @first_time.scaled_time_interval
end
- def test_network_address_values_ipaddr
- cidr_address = IPAddr.new '192.168.0.0/24'
- inet_address = IPAddr.new '172.16.1.254'
-
- assert_equal cidr_address, @first_network_address.cidr_address
- assert_equal inet_address, @first_network_address.inet_address
- 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
- end
-
def test_oid_values
assert_equal 1234, @first_oid.obj_id
end
- def test_update_integer_array
- new_value = [32800,95000,29350,17000]
- @first_array.commission_by_quarter = new_value
- assert @first_array.save
- assert @first_array.reload
- assert_equal new_value, @first_array.commission_by_quarter
- @first_array.commission_by_quarter = new_value
- assert @first_array.save
- assert @first_array.reload
- assert_equal new_value, @first_array.commission_by_quarter
- end
-
- def test_update_text_array
- new_value = ['robby','robert','rob','robbie']
- @first_array.nicknames = new_value
- assert @first_array.save
- assert @first_array.reload
- assert_equal new_value, @first_array.nicknames
- @first_array.nicknames = new_value
- assert @first_array.save
- assert @first_array.reload
- assert_equal new_value, @first_array.nicknames
- end
-
- def test_update_money
- new_value = BigDecimal.new('123.45')
- @first_money.wealth = new_value
- assert @first_money.save
- assert @first_money.reload
- assert_equal new_value, @first_money.wealth
- end
-
def test_update_number
new_single = 789.012
new_double = 789012.345
@@ -236,51 +87,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal '2 years 00:03:00', @first_time.time_interval
end
- def test_update_network_address
- 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'
- @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
- assert_equal @first_network_address.inet_address, new_inet_address
- assert_equal @first_network_address.mac_address, new_mac_address
- end
-
- def test_update_bit_string
- new_bit_string = '11111111'
- new_bit_string_varying = '0xFF'
- @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
- assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying
- end
-
- def test_invalid_hex_string
- new_bit_string = 'FF'
- @first_bit_string.bit_string = new_bit_string
- 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
@first_oid.obj_id = new_value
@@ -288,30 +94,26 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert @first_oid.reload
assert_equal new_value, @first_oid.obj_id
end
+end
- def test_timestamp_with_zone_values_with_rails_time_zone_support
- with_timezone_config default: :utc, aware_attributes: true do
- @connection.reconnect!
+class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase
+ include DdlHelper
- @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
- @connection.reconnect!
+ setup do
+ @connection = ActiveRecord::Base.connection
end
- def test_timestamp_with_zone_values_without_rails_time_zone_support
- 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')
+ def test_name_column_type
+ with_example_table @connection, 'ex', 'data name' do
+ column = @connection.columns('ex').find { |col| col.name == 'data' }
+ assert_equal :string, column.type
+ end
+ end
- @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
+ def test_char_column_type
+ with_example_table @connection, 'ex', 'data "char"' do
+ column = @connection.columns('ex').find { |col| col.name == 'data' }
+ assert_equal :string, column.type
end
- ensure
- @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..1500adb42d
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+
+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.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..d99c4a292e
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -0,0 +1,85 @@
+# -*- 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.binary?
+ assert_not column.array
+ end
+
+ def test_enum_defaults
+ @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy'
+ PostgresqlEnum.reset_column_information
+
+ assert_equal "happy", PostgresqlEnum.column_defaults['good_mood']
+ assert_equal "happy", PostgresqlEnum.new.good_mood
+ ensure
+ PostgresqlEnum.reset_column_information
+ 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/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
new file mode 100644
index 0000000000..7b99fcdda0
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -0,0 +1,63 @@
+require "cases/helper"
+
+class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class EnableHstore < ActiveRecord::Migration
+ def change
+ enable_extension "hstore"
+ end
+ end
+
+ class DisableHstore < ActiveRecord::Migration
+ def change
+ disable_extension "hstore"
+ end
+ end
+
+ def setup
+ super
+
+ @connection = ActiveRecord::Base.connection
+
+ unless @connection.supports_extensions?
+ return skip("no extension support")
+ end
+
+ @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name
+ @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix
+ @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix
+
+ ActiveRecord::Base.table_name_prefix = "p_"
+ ActiveRecord::Base.table_name_suffix = "_s"
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s"
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix
+ ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = true
+ ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name
+
+ super
+ end
+
+ def test_enable_extension_migration_ignores_prefix_and_suffix
+ @connection.disable_extension("hstore")
+
+ migrations = [EnableHstore.new(nil, 1)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled"
+ end
+
+ def test_disable_extension_migration_ignores_prefix_and_suffix
+ @connection.enable_extension("hstore")
+
+ migrations = [DisableHstore.new(nil, 1)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled"
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
new file mode 100644
index 0000000000..9dadb177ca
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -0,0 +1,26 @@
+# encoding: utf-8
+require "cases/helper"
+
+class PostgresqlFullTextTest < ActiveRecord::TestCase
+ class PostgresqlTsvector < ActiveRecord::Base; end
+
+ def test_tsvector_column
+ column = PostgresqlTsvector.columns_hash["text_vector"]
+ assert_equal :tsvector, column.type
+ assert_equal "tsvector", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_update_tsvector
+ PostgresqlTsvector.create text_vector: "'text' 'vector'"
+ tsvector = PostgresqlTsvector.first
+ assert_equal "'text' 'vector'", tsvector.text_vector
+
+ tsvector.text_vector = "'new' 'text' 'vector'"
+ tsvector.save!
+ assert tsvector.reload
+ assert_equal "'new' 'text' 'vector'", tsvector.text_vector
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
new file mode 100644
index 0000000000..6c0adbbeaa
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlPointTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include SchemaDumpingHelper
+
+ class PostgresqlPoint < ActiveRecord::Base; end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.create_table('postgresql_points') do |t|
+ t.point :x
+ t.point :y, default: [12.2, 13.3]
+ t.point :z, default: "(14.4,15.5)"
+ end
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_points'
+ end
+
+ def test_column
+ column = PostgresqlPoint.columns_hash["x"]
+ assert_equal :point, column.type
+ assert_equal "point", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y']
+ assert_equal [12.2, 13.3], PostgresqlPoint.new.y
+
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z']
+ assert_equal [14.4, 15.5], PostgresqlPoint.new.z
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_points")
+ assert_match %r{t\.point\s+"x"$}, output
+ assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output
+ assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output
+ end
+
+ def test_roundtrip
+ PostgresqlPoint.create! x: [10, 25.2]
+ record = PostgresqlPoint.first
+ assert_equal [10, 25.2], record.x
+
+ record.x = [1.1, 2.2]
+ record.save!
+ assert record.reload
+ assert_equal [1.1, 2.2], record.x
+ end
+
+ def test_mutation
+ p = PostgresqlPoint.create! x: [10, 20]
+
+ p.x[1] = 25
+ p.save!
+ p.reload
+
+ assert_equal [10.0, 25.0], p.x
+ assert_not p.changed?
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index d8782f5eaa..1296eb72c0 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -24,13 +24,14 @@ 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' }
+ @column = Hstore.columns_hash['tags']
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists hstores'
end
@@ -53,6 +54,20 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_column
assert_equal :hstore, @column.type
+ assert_equal "hstore", @column.sql_type
+ assert_not @column.number?
+ assert_not @column.binary?
+ assert_not @column.array
+ end
+
+ def test_default
+ @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
+ Hstore.reset_column_information
+
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions'])
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
+ ensure
+ Hstore.reset_column_information
end
def test_change_table_supports_hstore
@@ -61,7 +76,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
t.hstore 'users', default: ''
end
Hstore.reset_column_information
- column = Hstore.columns.find { |c| c.name == 'users' }
+ column = Hstore.columns_hash['users']
assert_equal :hstore, column.type
raise ActiveRecord::Rollback # reset the schema change
@@ -89,22 +104,17 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
def test_cast_value_on_write
x = Hstore.new tags: {"bool" => true, "number" => 5}
+ assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast)
assert_equal({"bool" => "true", "number" => "5"}, x.tags)
x.save
assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
end
def test_type_cast_hstore
- assert @column
-
- data = "\"1\"=>\"2\""
- hash = @column.class.string_to_hstore data
- assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, @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({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\""))
+ assert_equal({}, @column.type_cast_from_database(""))
+ assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b")))
end
def test_with_store_accessors
@@ -125,48 +135,78 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal "GMT", x.timezone
end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
+
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { 'one' => 'two' })
+ hstore.settings['three'] = 'four'
+ hstore.save!
+ hstore.reload
+
+ assert_equal 'four', hstore.settings['three']
+ assert_not hstore.changed?
+ end
+
def test_gen1
- assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
+ assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''}))
end
def test_gen2
- assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
+ assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''}))
end
def test_gen3
- assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
+ assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''}))
end
def test_gen4
- assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
+ assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''}))
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'))
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
end
def test_parse2
- assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
+ assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ "))
end
def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast("==>>"))
+ assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>"))
end
def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
+ assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w'))
end
def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
+ assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w'))
end
def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
+ assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w'))
end
def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
+ assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w'))
end
def test_rewrite
@@ -182,6 +222,30 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal({'1' => '2'}, x.tags)
end
+ def test_array_cycle
+ assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}])
+ end
+
+ def test_array_strings_with_quotes
+ assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}])
+ end
+
+ def test_array_strings_with_commas
+ assert_array_cycle([{'this,has' => 'many,values'}])
+ end
+
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(['{' => '}'])
+ 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
@@ -224,19 +288,51 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle("a\nb" => "c\nd")
end
- def test_update_all
- hstore = Hstore.create! tags: { "one" => "two" }
+ class TagCollection
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
- Hstore.update_all tags: { "three" => "four" }
- assert_equal({ "three" => "four" }, hstore.reload.tags)
+ class HstoreWithSerialize < Hstore
+ serialize :tags, TagCollection
+ end
+
+ def test_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
+ record = HstoreWithSerialize.first
+ assert_instance_of TagCollection, record.tags
+ assert_equal({"one" => "two"}, record.tags.to_hash)
+ record.tags = TagCollection.new("three" => "four")
+ record.save!
+ assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash)
+ end
- Hstore.update_all tags: { }
- assert_equal({ }, hstore.reload.tags)
+ def test_clone_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
+ record = HstoreWithSerialize.first
+ dupe = record.dup
+ assert_equal({"one" => "two"}, dupe.tags.to_hash)
end
end
private
+ 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)
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
new file mode 100644
index 0000000000..22e8873333
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -0,0 +1,44 @@
+require "cases/helper"
+
+class PostgresqlInfinityTest < ActiveRecord::TestCase
+ class PostgresqlInfinity < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:postgresql_infinities) do |t|
+ t.float :float
+ t.datetime :datetime
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS postgresql_infinities")
+ end
+
+ test "type casting infinity on a float column" do
+ record = PostgresqlInfinity.create!(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "update_all with infinity on a float column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+
+ test "update_all with infinity on a datetime column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index 3daef399d8..86ba849445 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -4,7 +4,7 @@ require "cases/helper"
require 'active_record/base'
require 'active_record/connection_adapters/postgresql_adapter'
-class PostgresqlJSONTest < ActiveRecord::TestCase
+module PostgresqlJSONSharedTestCases
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
@@ -16,14 +16,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('json_data_type') do |t|
- t.json 'payload', :default => {}
- t.json 'settings'
+ t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
+ t.public_send column_type, 'settings' # t.json 'settings'
end
end
rescue ActiveRecord::StatementInvalid
skip "do not test on PG without json"
end
- @column = JsonDataType.columns.find { |c| c.name == 'payload' }
+ @column = JsonDataType.columns_hash['payload']
end
def teardown
@@ -31,17 +31,32 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
end
def test_column
- assert_equal :json, @column.type
+ column = JsonDataType.columns_hash["payload"]
+ assert_equal column_type, column.type
+ assert_equal column_type.to_s, column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}'
+ JsonDataType.reset_column_information
+
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
+ ensure
+ JsonDataType.reset_column_information
end
def test_change_table_supports_json
@connection.transaction do
@connection.change_table('json_data_type') do |t|
- t.json 'users', default: '{}'
+ t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}'
end
JsonDataType.reset_column_information
- column = JsonDataType.columns.find { |c| c.name == 'users' }
- assert_equal :json, column.type
+ column = JsonDataType.columns_hash['users']
+ assert_equal column_type, column.type
raise ActiveRecord::Rollback # reset the schema change
end
@@ -51,22 +66,23 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
def test_cast_value_on_write
x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast)
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.type_cast_from_database(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_from_database(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_from_database("{}"))
+ assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"})))
end
def test_rewrite
@@ -122,13 +138,56 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal "640×1136", x.resolution
end
- def test_update_all
- json = JsonDataType.create! payload: { "one" => "two" }
+ def test_duplication_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = x.dup
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_changes_in_place
+ json = JsonDataType.new
+ assert_not json.changed?
+
+ json.payload = { 'one' => 'two' }
+ assert json.changed?
+ assert json.payload_changed?
+
+ json.save!
+ assert_not json.changed?
+
+ json.payload['three'] = 'four'
+ assert json.payload_changed?
+
+ json.save!
+ json.reload
+
+ assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
+ assert_not json.changed?
+ end
+end
+
+class PostgresqlJSONTest < ActiveRecord::TestCase
+ include PostgresqlJSONSharedTestCases
+
+ def column_type
+ :json
+ end
+end
- JsonDataType.update_all payload: { "three" => "four" }
- assert_equal({ "three" => "four" }, json.reload.payload)
+class PostgresqlJSONBTest < ActiveRecord::TestCase
+ include PostgresqlJSONSharedTestCases
- JsonDataType.update_all payload: { }
- assert_equal({ }, json.reload.payload)
+ def column_type
+ :jsonb
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index 5d12ca75ca..889e369bd6 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -1,7 +1,5 @@
# encoding: utf-8
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlLtreeTest < ActiveRecord::TestCase
class Ltree < ActiveRecord::Base
@@ -10,6 +8,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 +22,17 @@ 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.binary?
+ assert_not column.array
end
def test_write
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
new file mode 100644
index 0000000000..87183174f2
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -0,0 +1,96 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlMoneyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ class PostgresqlMoney < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("set lc_monetary = 'C'")
+ @connection.create_table('postgresql_moneys') do |t|
+ t.column "wealth", "money"
+ t.column "depth", "money", default: "150.55"
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys'
+ end
+
+ def test_column
+ column = PostgresqlMoney.columns_hash["wealth"]
+ assert_equal :money, column.type
+ assert_equal "money", column.sql_type
+ assert_equal 2, column.scale
+ assert column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth']
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
+ end
+
+ def test_money_values
+ @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)")
+ @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)")
+
+ first_money = PostgresqlMoney.find(1)
+ second_money = PostgresqlMoney.find(2)
+ assert_equal 567.89, first_money.wealth
+ assert_equal(-567.89, second_money.wealth)
+ end
+
+ def test_money_type_cast
+ column = PostgresqlMoney.columns_hash['wealth']
+ assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12"))
+ assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12"))
+ assert_equal(-1.15, column.type_cast_from_user("-$1.15"))
+ assert_equal(-2.25, column.type_cast_from_user("($2.25)"))
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_moneys")
+ assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output
+ assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output
+ end
+
+ def test_create_and_update_money
+ money = PostgresqlMoney.create(wealth: "987.65")
+ assert_equal 987.65, money.wealth
+
+ new_value = BigDecimal.new('123.45')
+ money.wealth = new_value
+ money.save!
+ money.reload
+ assert_equal new_value, money.wealth
+ end
+
+ def test_update_all_with_money_string
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: "987.65")
+ money.reload
+
+ assert_equal 987.65, money.wealth
+ end
+
+ def test_update_all_with_money_big_decimal
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: '123.45'.to_d)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+
+ def test_update_all_with_money_numeric
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: 123.45)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
new file mode 100644
index 0000000000..4f4c1103fa
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -0,0 +1,71 @@
+# encoding: utf-8
+require "cases/helper"
+
+class PostgresqlNetworkTest < ActiveRecord::TestCase
+ class PostgresqlNetworkAddress < ActiveRecord::Base
+ end
+
+ def test_cidr_column
+ column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
+ assert_equal :cidr, column.type
+ assert_equal "cidr", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_inet_column
+ column = PostgresqlNetworkAddress.columns_hash["inet_address"]
+ assert_equal :inet, column.type
+ assert_equal "inet", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_macaddr_column
+ column = PostgresqlNetworkAddress.columns_hash["mac_address"]
+ assert_equal :macaddr, column.type
+ assert_equal "macaddr", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_network_types
+ PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24',
+ inet_address: '172.16.1.254/32',
+ mac_address: '01:23:45:67:89:0a')
+
+ address = PostgresqlNetworkAddress.first
+ assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address
+ assert_equal IPAddr.new('172.16.1.254'), address.inet_address
+ assert_equal '01:23:45:67:89:0a', address.mac_address
+
+ address.cidr_address = '10.1.2.3/32'
+ address.inet_address = '10.0.0.0/8'
+ address.mac_address = 'bc:de:f0:12:34:56'
+
+ address.save!
+ assert address.reload
+ assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address
+ assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address
+ assert_equal 'bc:de:f0:12:34:56', address.mac_address
+ end
+
+ def test_invalid_network_address
+ invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr',
+ inet_address: 'invalid addr')
+ assert_nil invalid_address.cidr_address
+ assert_nil invalid_address.inet_address
+ assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast
+ assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast
+ assert invalid_address.save
+
+ invalid_address.reload
+ assert_nil invalid_address.cidr_address
+ assert_nil invalid_address.inet_address
+ assert_nil invalid_address.cidr_address_before_type_cast
+ assert_nil invalid_address.inet_address_before_type_cast
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 019406dd84..cfff1f980b 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,9 +1,14 @@
# 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
end
@@ -129,18 +134,18 @@ module ActiveRecord
end
def test_default_sequence_name
- assert_equal 'accounts_id_seq',
+ assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'),
@connection.default_sequence_name('accounts', 'id')
- assert_equal 'accounts_id_seq',
+ assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'),
@connection.default_sequence_name('accounts')
end
def test_default_sequence_name_bad_table
- assert_equal 'zomg_id_seq',
+ assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'),
@connection.default_sequence_name('zomg', 'id')
- assert_equal 'zomg_id_seq',
+ assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'),
@connection.default_sequence_name('zomg')
end
@@ -176,6 +181,51 @@ module ActiveRecord
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 PostgreSQL::Name.new("public", "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
with_example_table do
insert(@connection, 'number' => 10)
@@ -303,12 +353,77 @@ module ActiveRecord
assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
end
+ def test_columns_for_distinct_without_order_specifiers
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id"])
+
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
+
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
+ end
+
def test_raise_error_when_cannot_translate_exception
assert_raise TypeError do
@connection.send(:log, nil) { @connection.execute(nil) }
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
+
+ def test_unparsed_defaults_are_at_least_set_when_saving
+ with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do
+ number_klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'ex'
+ end
+ column = number_klass.columns_hash["number"]
+ assert_nil column.default
+ assert_nil column.default_function
+
+ first_number = number_klass.new
+ assert_nil first_number.number
+
+ first_number.save!
+ assert_equal 4, first_number.reload.number
+ end
+ end
+
private
def insert(ctx, data)
binds = data.map { |name, value|
@@ -324,12 +439,8 @@ module ActiveRecord
ctx.exec_insert(sql, 'SQL', binds)
end
- def with_example_table(definition = nil)
- definition ||= 'id serial primary key, number integer, data character varying(255)'
- @connection.exec_query("create table ex(#{definition})")
- yield
- ensure
- @connection.exec_query('drop table if exists ex')
+ 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
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 1122f8b9a1..11d5173d37 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -10,46 +10,46 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, Type::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, Type::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, 'varchar')
+ c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar')
assert_equal "'666'", @conn.quote(fixnum, c)
- c = Column.new(nil, nil, 'text')
+ c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text')
assert_equal "'666'", @conn.quote(fixnum, c)
end
@@ -57,6 +57,17 @@ module ActiveRecord
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
+
+ def test_quote_range
+ range = "1,2]'; SELECT * FROM users; --".."a"
+ c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range))
+ assert_equal "'[1,0]'", @conn.quote(range, c)
+ end
+
+ def test_quote_bit_string
+ c = PostgreSQLColumn.new(nil, 1, OID::Bit.new)
+ assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index 5c2d8e1c1d..d812cd01c4 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -1,6 +1,5 @@
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/connection_helper'
if ActiveRecord::Base.connection.supports_ranges?
class PostgresqlRange < ActiveRecord::Base
@@ -8,14 +7,20 @@ if ActiveRecord::Base.connection.supports_ranges?
end
class PostgresqlRangeTest < ActiveRecord::TestCase
- def teardown
- @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
- end
+ self.use_transactional_fixtures = false
+ include ConnectionHelper
def setup
- @connection = ActiveRecord::Base.connection
+ @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
@@ -24,7 +29,10 @@ if ActiveRecord::Base.connection.supports_ranges?
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
@@ -35,23 +43,26 @@ if ActiveRecord::Base.connection.supports_ranges?
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]")
+ int8_range: "[10, 100]",
+ float_range: "[0.5, 0.7]")
insert_range(id: 102,
- date_range: "(''2012-01-02'', ''2012-01-04'')",
+ 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)")
+ int4_range: "[1, 10)",
+ int8_range: "[10, 100)",
+ float_range: "[0.5, 0.7)")
insert_range(id: 103,
- date_range: "(''2012-01-02'',]",
+ 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,]")
+ int4_range: "[1,]",
+ int8_range: "[10,]",
+ float_range: "[0.5,]")
insert_range(id: 104,
date_range: "[,]",
@@ -59,15 +70,17 @@ if ActiveRecord::Base.connection.supports_ranges?
ts_range: "[,]",
tstz_range: "[,]",
int4_range: "[,]",
- int8_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)")
+ 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)
@@ -77,6 +90,12 @@ if ActiveRecord::Base.connection.supports_ranges?
@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
@@ -88,24 +107,24 @@ if ActiveRecord::Base.connection.supports_ranges?
def test_int4range_values
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 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 11...100, @second_range.int8_range
- assert_equal 11...Float::INFINITY, @third_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, 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 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
@@ -133,6 +152,14 @@ if ActiveRecord::Base.connection.supports_ranges?
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)
@@ -203,6 +230,55 @@ if ActiveRecord::Base.connection.supports_ranges?
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
+
+ def test_update_all_with_ranges
+ PostgresqlRange.create!
+
+ PostgresqlRange.update_all(int8_range: 1..100)
+
+ assert_equal 1...101, PostgresqlRange.first.int8_range
+ end
+
+ def test_ranges_correctly_escape_input
+ range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a"
+ PostgresqlRange.update_all(int8_range: range)
+
+ assert_nothing_raised do
+ PostgresqlRange.first
+ end
+ end
+
private
def assert_equal_round_trip(range, attribute, value)
round_trip(range, attribute, value)
@@ -229,7 +305,8 @@ if ActiveRecord::Base.connection.supports_ranges?
ts_range,
tstz_range,
int4_range,
- int8_range
+ int8_range,
+ float_range
) VALUES (
#{values[:id]},
'#{values[:date_range]}',
@@ -237,7 +314,8 @@ if ActiveRecord::Base.connection.supports_ranges?
'#{values[:ts_range]}',
'#{values[:tstz_range]}',
'#{values[:int4_range]}',
- '#{values[:int8_range]}'
+ '#{values[:int8_range]}',
+ '#{values[:float_range]}'
)
SQL
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 3f7009c1d1..9e5fd17dc4 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -50,6 +50,16 @@ class SchemaTest < ActiveRecord::TestCase
self.table_name = 'things'
end
+ class Song < ActiveRecord::Base
+ self.table_name = "music.songs"
+ has_and_belongs_to_many :albums
+ end
+
+ class Album < ActiveRecord::Base
+ self.table_name = "music.albums"
+ has_and_belongs_to_many :songs
+ end
+
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@@ -71,7 +81,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
@@ -109,6 +119,22 @@ class SchemaTest < ActiveRecord::TestCase
assert !@connection.schema_names.include?("test_schema3")
end
+ def test_habtm_table_name_with_schema
+ ActiveRecord::Base.connection.execute <<-SQL
+ DROP SCHEMA IF EXISTS music CASCADE;
+ CREATE SCHEMA music;
+ CREATE TABLE music.albums (id serial primary key);
+ CREATE TABLE music.songs (id serial primary key);
+ CREATE TABLE music.albums_songs (album_id integer, song_id integer);
+ SQL
+
+ song = Song.create
+ Album.create
+ assert_equal song, Song.includes(:albums).references(:albums).first
+ ensure
+ ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;"
+ end
+
def test_raise_drop_schema_with_nonexisting_schema
assert_raises(ActiveRecord::StatementInvalid) do
@connection.drop_schema "test_schema3"
@@ -271,13 +297,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
@@ -305,14 +331,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_pk_and_sequence_for_with_schema_specified
+ pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
[
%("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"),
%("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
].each do |given|
pk, seq = @connection.pk_and_sequence_for(given)
assert_equal 'id', pk, "primary key should be found when table referenced as #{given}"
- assert_equal "#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
- assert_equal "#{UNMATCHED_SEQUENCE_NAME}", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
end
end
@@ -328,18 +355,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?
@@ -353,6 +379,14 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+ def test_reset_pk_sequence
+ sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
+ @connection.execute "SELECT setval('#{sequence_name}', 123)"
+ assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ end
+
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 4d29a20e66..3614b29190 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -2,6 +2,47 @@ require 'cases/helper'
require 'models/developer'
require 'models/topic'
+class PostgresqlTimestampTest < ActiveRecord::TestCase
+ class PostgresqlTimestampWithZone < ActiveRecord::Base; end
+
+ self.use_transactional_fixtures = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')")
+ end
+
+ teardown do
+ PostgresqlTimestampWithZone.delete_all
+ end
+
+ def test_timestamp_with_zone_values_with_rails_time_zone_support
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
+
+ timestamp = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_instance_of Time, timestamp.time
+ end
+ ensure
+ @connection.reconnect!
+ end
+
+ def test_timestamp_with_zone_values_without_rails_time_zone_support
+ 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')
+
+ timestamp = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_instance_of Time, timestamp.time
+ end
+ ensure
+ @connection.reconnect!
+ end
+end
+
class TimestampTest < ActiveRecord::TestCase
fixtures :topics
@@ -82,20 +123,32 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal date, Developer.find_by_name("aaron").updated_at
end
+ def test_bc_timestamp_leap_year
+ date = Time.utc(-4, 2, 29)
+ Developer.create!(:name => "taihou", :updated_at => date)
+ assert_equal date, Developer.find_by_name("taihou").updated_at
+ end
+
+ def test_bc_timestamp_year_zero
+ date = Time.utc(0, 4, 7)
+ Developer.create!(:name => "yahagi", :updated_at => date)
+ assert_equal date, Developer.find_by_name("yahagi").updated_at
+ end
+
private
- def pg_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
+ def pg_datetime_precision(table_name, column_name)
+ results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
end
+ result && result["datetime_precision"]
+ end
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
+ def activerecord_column_option(tablename, column_name, option)
+ result = ActiveRecord::Base.connection.columns(tablename).find do |column|
+ column.name == column_name
end
+ result && result.send(option)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
new file mode 100644
index 0000000000..23817198b1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -0,0 +1,15 @@
+require 'cases/helper'
+
+class PostgresqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test "array delimiters are looked up correctly" do
+ box_array = @connection.type_map.lookup(1020)
+ int_array = @connection.type_map.lookup(1007)
+
+ assert_equal ';', box_array.delimiter
+ assert_equal ',', int_array.delimiter
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 9e7b08ef34..3fdb6888d9 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -1,9 +1,10 @@
require 'cases/helper'
class PostgreSQLUtilsTest < ActiveSupport::TestCase
- include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
+ include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
- def test_extract_schema_and_table
+ def test_extract_schema_qualified_name
{
%(table_name) => [nil,'table_name'],
%("table.name") => [nil,'table.name'],
@@ -14,7 +15,47 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase
%("even spaces".table) => ['even spaces','table'],
%(schema."table.name") => ['schema', 'table.name']
}.each do |given, expect|
- assert_equal expect, extract_schema_and_table(given)
+ assert_equal Name.new(*expect), extract_schema_qualified_name(given)
end
end
end
+
+class PostgreSQLNameTest < ActiveSupport::TestCase
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
+
+ test "represents itself as schema.name" do
+ obj = Name.new("public", "articles")
+ assert_equal "public.articles", obj.to_s
+ end
+
+ test "without schema, represents itself as name only" do
+ obj = Name.new(nil, "articles")
+ assert_equal "articles", obj.to_s
+ end
+
+ test "quoted returns a string representation usable in a query" do
+ assert_equal %("articles"), Name.new(nil, "articles").quoted
+ assert_equal %("public"."articles"), Name.new("public", "articles").quoted
+ end
+
+ test "prevents double quoting" do
+ name = Name.new('"quoted_schema"', '"quoted_table"')
+ assert_equal "quoted_schema.quoted_table", name.to_s
+ assert_equal %("quoted_schema"."quoted_table"), name.quoted
+ end
+
+ test "equality based on state" do
+ assert_equal Name.new("access", "users"), Name.new("access", "users")
+ assert_equal Name.new(nil, "users"), Name.new(nil, "users")
+ assert_not_equal Name.new(nil, "users"), Name.new("access", "users")
+ assert_not_equal Name.new("access", "users"), Name.new("public", "users")
+ assert_not_equal Name.new("public", "users"), Name.new("public", "articles")
+ end
+
+ test "can be used as hash key" do
+ hash = {Name.new("schema", "article_seq") => "success"}
+ assert_equal "success", hash[Name.new("schema", "article_seq")]
+ assert_equal nil, hash[Name.new("schema", "articles")]
+ assert_equal nil, hash[Name.new("public", "article_seq")]
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 3f5d981444..66006d718f 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -4,31 +4,146 @@ require "cases/helper"
require 'active_record/base'
require 'active_record/connection_adapters/postgresql_adapter'
+module PostgresqlUUIDHelper
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+
+ def drop_table(name)
+ connection.execute "drop table if exists #{name}"
+ end
+end
+
class PostgresqlUUIDTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
+ class UUIDType < ActiveRecord::Base
+ self.table_name = "uuid_data_type"
+ end
+
+ setup do
+ connection.create_table "uuid_data_type" do |t|
+ t.uuid 'guid'
+ end
+ end
+
+ teardown do
+ drop_table "uuid_data_type"
+ end
+
+ 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_hash['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_hash['thingy']
+ assert_equal "uuid_generate_v4()", column.default_function
+ ensure
+ UUIDType.reset_column_information
+ end
+
+ 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.binary?
+ assert_not column.array
+ end
+
+ def test_treat_blank_uuid_as_nil
+ UUIDType.create! guid: ''
+ assert_equal(nil, UUIDType.last.guid)
+ end
+
+ def test_treat_invalid_uuid_as_nil
+ uuid = UUIDType.create! guid: 'foobar'
+ assert_equal(nil, uuid.guid)
+ end
+
+ def test_invalid_uuid_dont_modify_before_type_cast
+ uuid = UUIDType.new guid: 'foobar'
+ assert_equal 'foobar', uuid.guid_before_type_cast
+ end
+
+ def test_rfc_4122_regex
+ # Valid uuids
+ ['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|
+ uuid = UUIDType.new guid: valid_uuid
+ assert_not_nil uuid.guid
+ end
+
+ # Invalid uuids
+ [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'],
+ Hash.new,
+ 0,
+ 0.0,
+ true,
+ 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11',
+ '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}',
+ 'a0eebc999r0b4ef8ab6d6bb9bd380a11',
+ 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11',
+ '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid|
+ uuid = UUIDType.new guid: invalid_uuid
+ assert_nil uuid.guid
+ end
+ end
+
+ 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 PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
class UUID < ActiveRecord::Base
self.table_name = 'pg_uuids'
end
- def setup
- @connection = ActiveRecord::Base.connection
+ setup do
+ enable_uuid_ossp!(connection)
- unless @connection.extension_enabled?('uuid-ossp')
- @connection.enable_extension 'uuid-ossp'
- @connection.commit_db_transaction
+ 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
- @connection.reconnect!
+ # Create custom PostgreSQL function to generate UUIDs
+ # to test dumping tables which columns have defaults with custom functions
+ connection.execute <<-SQL
+ CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
+ AS $$ SELECT * FROM uuid_generate_v4() $$
+ LANGUAGE SQL VOLATILE;
+ SQL
- @connection.transaction do
- @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
+ # Create such a table with custom function as default value generator
+ connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t|
+ t.string 'name'
+ t.uuid 'other_uuid_2', default: 'my_uuid_generator()'
end
end
- def teardown
- @connection.execute 'drop table if exists pg_uuids'
+ teardown do
+ drop_table "pg_uuids"
+ drop_table 'pg_uuids_2'
+ connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();'
end
if ActiveRecord::Base.connection.supports_extensions?
@@ -49,49 +164,46 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase
end
def test_pk_and_sequence_for_uuid_primary_key
- pk, seq = @connection.pk_and_sequence_for('pg_uuids')
+ 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)
+ 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
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = StringIO.new
+ ActiveRecord::SchemaDumper.dump(connection, schema)
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string)
+ end
end
end
class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
- class UUID < ActiveRecord::Base
- self.table_name = 'pg_uuids'
- end
+ include PostgresqlUUIDHelper
- def setup
- @connection = ActiveRecord::Base.connection
- @connection.reconnect!
+ setup do
+ enable_uuid_ossp!(connection)
- unless @connection.extension_enabled?('uuid-ossp')
- @connection.enable_extension 'uuid-ossp'
- @connection.commit_db_transaction
- end
-
- @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: 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
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
+ 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
@@ -101,6 +213,8 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
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
@@ -111,30 +225,24 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
belongs_to :uuid_post
end
- def setup
- @connection = ActiveRecord::Base.connection
- @connection.reconnect!
-
- unless @connection.extension_enabled?('uuid-ossp')
- @connection.enable_extension 'uuid-ossp'
- @connection.commit_db_transaction
- end
+ setup do
+ enable_uuid_ossp!(connection)
- @connection.transaction do
- @connection.create_table('pg_uuid_posts', id: :uuid) do |t|
+ 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()'
+ connection.create_table('pg_uuid_comments', id: :uuid) do |t|
+ t.references :uuid_post, type: :uuid
t.string 'content'
end
end
end
- def teardown
- @connection.transaction do
- @connection.execute 'drop table if exists pg_uuid_comments'
- @connection.execute 'drop table if exists pg_uuid_posts'
+ teardown do
+ connection.transaction do
+ drop_table "pg_uuid_comments"
+ drop_table "pg_uuid_posts"
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
index dd2a727afe..4165dd5ac9 100644
--- a/activerecord/test/cases/adapters/postgresql/xml_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -1,8 +1,5 @@
# 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
@@ -14,16 +11,16 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('xml_data_type') do |t|
- t.xml 'payload', default: {}
+ t.xml 'payload'
end
end
rescue ActiveRecord::StatementInvalid
skip "do not test on PG without xml"
end
- @column = XmlDataType.columns.find { |c| c.name == 'payload' }
+ @column = XmlDataType.columns_hash['payload']
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists xml_data_type'
end
@@ -35,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase
@connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
assert_nil XmlDataType.first.payload
end
+
+ def test_round_trip
+ data = XmlDataType.new(payload: "<foo>bar</foo>")
+ assert_equal "<foo>bar</foo>", data.payload
+ data.save!
+ assert_equal "<foo>bar</foo>", data.reload.payload
+ end
+
+ def test_update_all
+ data = XmlDataType.create!
+ XmlDataType.update_all(payload: "<bar>baz</bar>")
+ assert_equal "<bar>baz</bar>", data.reload.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 b478db749d..13b754d226 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -60,7 +60,6 @@ class CopyTableTest < ActiveRecord::TestCase
assert_equal original_id.type, copied_id.type
assert_equal original_id.sql_type, copied_id.sql_type
assert_equal original_id.limit, copied_id.limit
- assert_equal original_id.primary, copied_id.primary
end
end
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 ba89487838..ac8332e2fa 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -15,7 +15,7 @@ module ActiveRecord
def test_type_cast_binary_encoding_without_logger
@conn.extend(Module.new { def logger; end })
- column = Struct.new(:type, :name).new(:string, "foo")
+ column = Column.new(nil, nil, Type::String.new)
binary = SecureRandom.hex
expected = binary.dup.encode!(Encoding::UTF_8)
assert_equal expected, @conn.type_cast(binary, column)
@@ -47,13 +47,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 't', @conn.type_cast(true, nil)
assert_equal 1, @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 'f', @conn.type_cast(false, nil)
assert_equal 0, @conn.type_cast(false, c)
end
@@ -61,16 +61,16 @@ module ActiveRecord
def test_type_cast_string
assert_equal '10', @conn.type_cast('10', nil)
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 10, @conn.type_cast('10', c)
- c = Column.new(nil, 1, 'float')
+ c = Column.new(nil, 1, Type::Float.new)
assert_equal 10.1, @conn.type_cast('10.1', c)
- c = Column.new(nil, 1, 'binary')
+ c = Column.new(nil, 1, Type::Binary.new)
assert_equal '10.1', @conn.type_cast('10.1', c)
- c = Column.new(nil, 1, 'date')
+ c = Column.new(nil, 1, Type::Date.new)
assert_equal '10.1', @conn.type_cast('10.1', c)
end
@@ -84,7 +84,7 @@ module ActiveRecord
assert_raise(TypeError) { @conn.type_cast(obj, nil) }
end
- def test_quoted_id
+ def test_type_cast_object_which_responds_to_quoted_id
quoted_id_obj = Class.new {
def quoted_id
"'zomg'"
@@ -100,9 +100,16 @@ module ActiveRecord
def quoted_id
"'zomg'"
end
- }
+ }.new
assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) }
end
+
+ def test_quoting_binary_strings
+ value = "hello".encode('ascii-8bit')
+ column = Column.new(nil, 1, SQLite3String.new)
+
+ assert_equal "'hello'", @conn.quote(value, column)
+ end
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 02834edf7b..b2bf9480dd 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -2,28 +2,22 @@
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
-
- @subscriber = SQLSubscriber.new
- ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ @conn = Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: 100
end
def test_bad_connection
@@ -33,12 +27,11 @@ module ActiveRecord
end
end
- def test_connect_with_url
- skip "can't establish new connection when using memory db" if in_memory_db?
- begin
+ unless in_memory_db?
+ def test_connect_with_url
original_connection = ActiveRecord::Base.remove_connection
tf = Tempfile.open 'whatever'
- url = "sqlite3://#{tf.path}"
+ url = "sqlite3:#{tf.path}"
ActiveRecord::Base.establish_connection(url)
assert ActiveRecord::Base.connection
ensure
@@ -46,13 +39,10 @@ module ActiveRecord
tf.unlink
ActiveRecord::Base.establish_connection(original_connection)
end
- end
- def test_connect_memory_with_url
- skip "can't establish new connection when using memory db" if in_memory_db?
- begin
+ def test_connect_memory_with_url
original_connection = ActiveRecord::Base.remove_connection
- url = "sqlite3:///:memory:"
+ url = "sqlite3::memory:"
ActiveRecord::Base.establish_connection(url)
assert ActiveRecord::Base.connection
ensure
@@ -61,25 +51,23 @@ module ActiveRecord
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
- # even if the type is not valid.
+ # sqlite3 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 even if the
+ # type is not valid.
def test_invalid_column
assert @conn.valid_type?(:foobar)
end
- def teardown
- ActiveSupport::Notifications.unsubscribe(@subscriber)
- super
- 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
@@ -89,23 +77,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
@@ -116,17 +109,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
@@ -145,77 +138,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 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
@@ -223,128 +222,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 @subscriber.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', @subscriber.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('@subscriber.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/, @subscriber.logged.last.first)
end
def test_no_indexes
@@ -352,41 +364,45 @@ 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
@@ -401,13 +417,39 @@ module ActiveRecord
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, @subscriber.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/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 500df52cd8..8700b20dee 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -10,13 +10,27 @@ 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
ActiveRecord::SchemaMigration.delete_all rescue nil
end
+ def test_has_no_primary_key
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ assert_nil ActiveRecord::SchemaMigration.primary_key
+
+ ActiveRecord::SchemaMigration.create_table
+ assert_difference "ActiveRecord::SchemaMigration.count", 1 do
+ ActiveRecord::SchemaMigration.create version: 12
+ end
+ ensure
+ ActiveRecord::SchemaMigration.drop_table
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
+ end
+
def test_schema_define
ActiveRecord::Schema.define(:version => 7) do
create_table :fruits do |t|
@@ -34,6 +48,7 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_schema_define_w_table_name_prefix
table_name = ActiveRecord::SchemaMigration.table_name
+ old_table_name_prefix = ActiveRecord::Base.table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
ActiveRecord::Schema.define(:version => 7) do
@@ -46,7 +61,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
assert_equal 7, ActiveRecord::Migrator::current_version
ensure
- ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_prefix = old_table_name_prefix
ActiveRecord::SchemaMigration.table_name = table_name
end
@@ -66,5 +81,12 @@ if ActiveRecord::Base.connection.supports_migrations?
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
end
+
+ def test_normalize_version
+ assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118")
+ assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2")
+ assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
+ assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
+ end
end
end
diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb
index c78b036f53..3e0032ec73 100644
--- a/activerecord/test/cases/associations/association_scope_test.rb
+++ b/activerecord/test/cases/associations/association_scope_test.rb
@@ -9,7 +9,12 @@ module ActiveRecord
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 9340bc0a83..25555bd75c 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -16,6 +16,8 @@ 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,
@@ -231,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
@@ -340,6 +342,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
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])
@@ -356,6 +369,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_queries(2) { line_item.update amount: 10 }
end
+ def test_belongs_to_with_touch_option_on_empty_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(0) { line_item.save }
+ end
+
def test_belongs_to_with_touch_option_on_destroy
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
@@ -489,6 +509,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]
@@ -529,6 +570,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert companies(:first_client).readonly_firm.readonly?
end
+ def test_test_polymorphic_assignment_foreign_key_type_string
+ comment = Comment.first
+ comment.author = Author.first
+ comment.resource = Member.first
+ comment.save
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:author).to_a
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:resource).to_a
+ end
+
def test_polymorphic_assignment_foreign_type_field_updating
# should update when assigning a saved record
sponsor = Sponsor.new
@@ -733,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.taggings_count }, -1 do
- assert_difference 'comment.reload.taggings_count', +1 do
+ assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_difference 'comment.reload.tags_count', +1 do
tagging.taggable = comment
end
end
@@ -824,6 +878,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)
@@ -863,4 +928,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
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
+
+class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses
+
+ def test_destroy_linked_models
+ address = AuthorAddress.create!
+ author = Author.create! name: "Author", author_address_id: address.id
+
+ author.destroy!
+ 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 71c0609df5..51d8e0523e 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
assert_nothing_raised do
- Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
end
- authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
assert_equal 1, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
new file mode 100644
index 0000000000..48f7ddbe83
--- /dev/null
+++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase
+ class Post < ActiveRecord::Base
+ has_many :taggings, as: :taggable
+ has_many :tags, through: :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ belongs_to :taggable, polymorphic: true
+ belongs_to :tag
+ end
+
+ class Tag < ActiveRecord::Base
+ end
+
+ test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do
+ post = Post.create!(title: 'Hello', body: 'World!')
+ assert_deprecated { post.tags << Tag.create!(name: 'whatever') }
+
+ assert_equal 1, post.tags.size
+ assert_equal 1, post.tags_count
+ assert_equal 1, post.reload.tags.size
+ assert_equal 1, post.reload.tags_count
+ 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 669569a774..a61a070331 100644
--- a/activerecord/test/cases/associations/eager_singularization_test.rb
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -1,6 +1,7 @@
require "cases/helper"
+if ActiveRecord::Base.connection.supports_migrations?
class EagerSingularizationTest < ActiveRecord::TestCase
class Virus < ActiveRecord::Base
belongs_to :octopus
@@ -50,8 +51,6 @@ class EagerSingularizationTest < ActiveRecord::TestCase
end
def setup
- skip 'Does not support migrations' unless connection.supports_migrations?
-
connection.create_table :viri do |t|
t.column :octopus_id, :integer
t.column :species, :string
@@ -91,7 +90,7 @@ class EagerSingularizationTest < ActiveRecord::TestCase
end
end
- def teardown
+ teardown do
connection.drop_table :viri
connection.drop_table :octopi
connection.drop_table :passes
@@ -146,3 +145,4 @@ class EagerSingularizationTest < ActiveRecord::TestCase
end
end
end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 498a4e8144..21912fdf0f 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -235,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
@@ -407,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
@@ -523,21 +534,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_conditions
- if current_adapter?(:OpenBaseAdapter)
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a
- else
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
- end
+ posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
def test_eager_with_has_many_and_limit_and_conditions_array
- if current_adapter?(:OpenBaseAdapter)
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a
- else
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
- end
+ posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
@@ -709,16 +712,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
@@ -815,11 +818,15 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preload_with_interpolation
- post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ assert_deprecated do
+ post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
+ assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ end
- post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ assert_deprecated do
+ post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
+ assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ end
end
def test_polymorphic_type_condition
@@ -925,13 +932,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_count_with_include
- if current_adapter?(:SybaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count
- elsif current_adapter?(:OpenBaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count
- else
- assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
- end
+ assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
end
def test_load_with_sti_sharing_association
@@ -1166,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
)
end
+ test "deep preload" do
+ post = Post.preload(author: :posts, comments: :post).first
+
+ assert_predicate post.author.association(:posts), :loaded?
+ assert_predicate post.comments.first.association(:post), :loaded?
+ end
+
test "preloading does not cache has many association subset when preloaded with a through association" do
author = Author.includes(:comments_with_order_and_conditions, :posts).first
assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size }
@@ -1187,6 +1195,23 @@ class EagerAssociationTest < ActiveRecord::TestCase
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 the same association twice works" do
+ Member.create!
+ members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a
+ assert_no_queries {
+ members_with_membership = members.select(&:current_membership)
+ assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size
+ }
+ end
+
test "preloading with a polymorphic association and using the existential predicate" do
assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
@@ -1194,4 +1219,75 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
+
+ test "including association based on sql condition and no database column" do
+ assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet
+ end
+
+ test "include instance dependent associations is deprecated" do
+ message = "association scope 'posts_with_signature' is"
+ assert_deprecated message do
+ begin
+ Author.includes(:posts_with_signature).to_a
+ rescue NoMethodError
+ # it's expected that preloading of this association fails
+ end
+ end
+
+ assert_deprecated message do
+ Author.preload(:posts_with_signature).to_a rescue NoMethodError
+ end
+
+ assert_deprecated message do
+ Author.eager_load(:posts_with_signature).to_a
+ end
+ end
+
+ test "preloading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").preload(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").preload(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").preload(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
+
+ test "eager-loading readonly association" do
+ skip "eager_load does not yet preserve readonly associations"
+ # has-one
+ firm = Firm.where(id: "1").eager_load(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").eager_load(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ 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 bac1cb8e2d..cc58a4a1a2 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,10 @@ require 'models/membership'
require 'models/sponsor'
require 'models/country'
require 'models/treaty'
+require 'models/vertex'
+require 'models/publisher'
+require 'models/publisher/article'
+require 'models/publisher/magazine'
require 'active_support/core_ext/string/conversions'
class ProjectWithAfterCreateHook < ActiveRecord::Base
@@ -65,6 +70,14 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
:foreign_key => "developer_id"
end
+class SubDeveloper < Developer
+ self.table_name = 'developers'
+ has_and_belongs_to_many :special_projects,
+ :join_table => 'developers_projects',
+ :foreign_key => "project_id",
+ :association_foreign_key => "developer_id"
+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
@@ -81,6 +94,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
country.treaties << treaty
end
+ def test_marshal_dump
+ post = posts :welcome
+ preloaded = Post.includes(:categories).find post.id
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
def test_should_property_quote_string_primary_keys
setup_data_for_habtm_case
@@ -215,6 +234,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
end
+ def test_habtm_collection_size_from_build
+ devel = Developer.create("name" => "Fred Wu")
+ devel.projects << Project.create("name" => "Grimetime")
+ devel.projects.build
+
+ assert_equal 2, devel.projects.size
+ end
+
+ def test_habtm_collection_size_from_params
+ devel = Developer.new({
+ projects_attributes: {
+ '0' => {}
+ }
+ })
+
+ assert_equal 1, devel.projects.size
+ end
+
def test_build
devel = Developer.find(1)
proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
@@ -785,7 +822,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal [], Pirate.where(id: redbeard.id)
end
- test "has and belongs to many associations on new records use null relations" do
+ def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
assert_no_queries do
assert_equal [], projects
@@ -795,4 +832,55 @@ 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
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
+ magazine = Publisher::Magazine.create
+ article = Publisher::Article.create
+ magazine.articles << article
+ magazine.save
+
+ assert_includes magazine.articles, article
+ end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model
+ article = Publisher::Article.create
+ tag = Tag.create
+ article.tags << tag
+ article.save
+
+ assert_includes article.tags, tag
+ end
+
+ def test_redefine_habtm
+ child = SubDeveloper.new("name" => "Aredridel")
+ child.special_projects << SpecialProject.new("name" => "Special Project")
+ assert child.save, 'child object should be saved'
+ 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 321440cab7..fe961e871c 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -22,6 +22,10 @@ require 'models/engine'
require 'models/categorization'
require 'models/minivan'
require 'models/speedometer'
+require 'models/reference'
+require 'models/job'
+require 'models/college'
+require 'models/student'
require 'models/pirate'
require 'models/ship'
@@ -32,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
author = authors(:david)
# this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
# if the reorder clauses are not correctly handled
- assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last
+ assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last
end
end
@@ -41,12 +45,18 @@ 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'
@@ -65,6 +75,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -109,6 +126,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -729,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_counter_cache_updates_in_memory_after_concat
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create_with_array
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!([
+ { title: "re: zoom", content: "speedy quick!" },
+ { title: "re: zoom 2", content: "OMG lol!" },
+ ])
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.replies.size
+ assert_equal 2, topic.reload.replies.size
+ end
+
def test_pushing_association_updates_counter_cache
topic = Topic.order("id ASC").first
reply = Reply.create!
@@ -741,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_without_dependent_option
post = posts(:welcome)
- assert_difference "post.reload.taggings_count", -1 do
+ assert_difference "post.reload.tags_count", -1 do
post.taggings.delete(post.taggings.first)
end
end
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
- post.update_columns(taggings_with_delete_all_count: post.taggings_count)
+ post.update_columns(taggings_with_delete_all_count: post.tags_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -757,13 +830,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
- post.update_columns(taggings_with_destroy_count: post.taggings_count)
+ post.update_columns(taggings_with_destroy_count: post.tags_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
end
end
+ def test_calling_empty_with_counter_cache
+ post = posts(:welcome)
+ assert_queries(0) do
+ assert_not post.comments.empty?
+ end
+ end
+
def test_custom_named_counter_cache
topic = topics(:first)
@@ -1244,6 +1324,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)
@@ -1833,11 +1923,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test 'has_many_association passes context validation to validate children' do
+ test 'passes custom context validation to validate children' do
pirate = FamousPirate.new
pirate.famous_ships << ship = FamousShip.new
- assert_equal true, pirate.valid?
- assert_equal false, pirate.valid?(:conference)
+
+ assert pirate.valid?
+ assert_not pirate.valid?(:conference)
assert_equal "can't be blank", ship.errors[:name].first
end
+
+ test 'association with instance dependent scope' do
+ bob = authors(:bob)
+ Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob))
+ Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob))
+ assert_equal ["misc post by bob", "other post by bob",
+ "signed post by bob"], bob.posts_with_signature.map(&:title).sort
+
+ assert_equal [], authors(:david).posts_with_signature.map(&:title)
+ 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 47592f312e..a85e020f0c 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.single_people.include?(person)
end
+ def test_both_parent_ids_set_when_saving_new
+ post = Post.new(title: 'Hello', body: 'world')
+ person = Person.new(first_name: 'Sean')
+
+ post.people = [person]
+ post.save
+
+ assert post.id
+ assert person.id
+ assert_equal post.id, post.readers.first.post_id
+ assert_equal person.id, post.readers.first.person_id
+ end
+
def test_delete_association
assert_queries(2){posts(:welcome);people(:michael); }
@@ -476,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do
+ assert_difference ['post.reload.tags_count'], -1 do
posts(:welcome).tags.delete(tag)
end
end
@@ -486,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_destroy_count: post.tags.count)
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
+ assert_difference ['post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
end
end
@@ -496,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_nullify_count: post.tags.count)
- assert_no_difference 'post.reload.taggings_count' do
+ assert_no_difference 'post.reload.tags_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do
posts(:welcome).tags_with_nullify.delete(tag)
end
@@ -511,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag.tagged_posts = []
post.reload
- assert_equal(post.taggings.count, post.taggings_count)
+ assert_equal(post.taggings.count, post.tags_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
+ assert_difference 'post.reload.tags_count', -1 do
tag.tagged_posts.destroy(post)
end
end
@@ -698,9 +711,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
@@ -817,6 +827,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")
@@ -1075,7 +1092,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
@@ -1087,7 +1104,7 @@ 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
@@ -1098,4 +1115,55 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
+
+ def test_has_many_through_add_with_sti_middle_relation
+ club = SuperClub.create!(name: 'Fight Club')
+ member = Member.create!(name: 'Tyler Durden')
+
+ club.members << member
+ assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
+ end
+
+ class ClubWithCallbacks < ActiveRecord::Base
+ self.table_name = 'clubs'
+ after_create :add_a_member
+
+ has_many :memberships, inverse_of: :club, foreign_key: :club_id
+ has_many :members, through: :memberships
+
+ def add_a_member
+ members << Member.last
+ end
+ end
+
+ def test_has_many_with_callback_before_association
+ Member.create!
+ club = ClubWithCallbacks.create!
+
+ assert_equal 1, club.reload.memberships.count
+ 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 a2725441b3..089cb0a3a2 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_creating_association_sets_both_parent_ids_for_new
+ member = Member.new(name: 'Sean Griffin')
+ club = Club.new(name: 'Da Club')
+
+ member.club = club
+
+ member.save!
+
+ assert member.id
+ assert club.id
+ assert_equal member.id, member.current_membership.member_id
+ assert_equal club.id, member.current_membership.club_id
+ end
+
def test_replace_target_record
new_club = Club.create(:name => "Marx Bros")
@member.club = new_club
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index a9efa6d86a..07cf65a760 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -117,4 +117,23 @@ 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
+
+ test "the correct records are loaded when including an aliased association" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: 'Not Special'
+ author.special_categories.create! name: 'Special'
+
+ categories = author.categories.eager_load(:special_categorizations).order(:name).to_a
+ assert_equal 0, categories.first.special_categorizations.size
+ assert_equal 1, categories.second.special_categorizations.size
+ end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 893030345f..60df4e14dd 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
assert_respond_to club_reflection, :has_inverse?
assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
end
+
+ def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name
+ man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
+ face_reflection = Face.reflect_on_association(:man)
+
+ assert_respond_to face_reflection, :has_inverse?
+ assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically"
+ end
end
class InverseAssociationTests < ActiveRecord::TestCase
@@ -333,7 +344,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_within_create_block_of_new_child
man = Man.first
- interest = man.interests.build do |i|
+ interest = man.interests.create do |i|
assert i.man.equal?(man), "Man of child should be the same instance as a parent"
end
assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent"
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index aabeea025f..cace7ba142 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_polymorphic_with_counter_cache
- assert_equal 1, posts(:welcome)[:taggings_count]
+ assert_equal 1, posts(:welcome)[:tags_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
- assert_equal 2, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 2, posts(:welcome, :reload)[:tags_count]
tagging.destroy
- assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 1, posts(:welcome, :reload)[:tags_count]
end
def test_unavailable_through_reflection
@@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 1, post_thinking.tags.size)
+ assert_equal(count + 1, post_thinking.reload.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
@@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 2, post_thinking.tags.size)
+ assert_equal(count + 2, post_thinking.reload.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
@@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 4, post_thinking.tags.size)
+ assert_equal(count + 4, post_thinking.reload.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
@@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(count, post_thinking.taggings(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 8ef351cda8..31b68c940e 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
mustache = sponsors(:moustache_club_sponsor_for_groucho)
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [mustache], members.first.nested_sponsors
end
end
@@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
+ ActiveRecord::Base.connection.table_alias_length # preheat cache
members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
new file mode 100644
index 0000000000..a6934a056e
--- /dev/null
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -0,0 +1,82 @@
+require "cases/helper"
+
+class RequiredAssociationsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Parent < ActiveRecord::Base
+ end
+
+ class Child < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :parents, force: true
+ @connection.create_table :children, force: true do |t|
+ t.belongs_to :parent
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS parents")
+ @connection.execute("DROP TABLE IF EXISTS children")
+ end
+
+ test "belongs_to associations are not required by default" do
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ end
+
+ test "required belongs_to associations have presence validated" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent can't be blank"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ end
+
+ test "has_one associations are not required by default" do
+ model = subclass_of(Parent) do
+ has_one :child, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ assert model.new.save
+ assert model.new(child: Child.new).save
+ end
+
+ test "required has_one associations have presence validated" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Child can't be blank"], record.errors.full_messages
+
+ record.child = Child.new
+ assert record.save
+ end
+
+ private
+
+ def subclass_of(klass, &block)
+ subclass = Class.new(klass, &block)
+ def subclass.name
+ superclass.name
+ end
+ subclass
+ end
+end
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
new file mode 100644
index 0000000000..cbc2c4e5d7
--- /dev/null
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -0,0 +1,124 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeDecoratorsTest < ActiveRecord::TestCase
+ class Model < ActiveRecord::Base
+ self.table_name = 'attribute_decorators_model'
+ end
+
+ class StringDecorator < SimpleDelegator
+ def initialize(delegate, decoration = "decorated!")
+ @decoration = decoration
+ super(delegate)
+ end
+
+ def type_cast_from_user(value)
+ "#{super} #{@decoration}"
+ end
+
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :attribute_decorators_model, force: true do |t|
+ t.string :a_string
+ end
+ end
+
+ teardown do
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model'
+ Model.attribute_type_decorations.clear
+ Model.reset_column_information
+ end
+
+ test "attributes can be decorated" do
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello', model.a_string
+
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello decorated!', model.a_string
+ end
+
+ test "decoration does not eagerly load existing columns" do
+ assert_no_queries do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ end
+ end
+
+ test "undecorated columns are not touched" do
+ Model.attribute :another_string, Type::String.new, default: 'something or other'
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ assert_equal 'something or other', Model.new.another_string
+ end
+
+ test "decorators can be chained" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated! decorated!', model.a_string
+ end
+
+ test "decoration of the same type multiple times is idempotent" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello decorated!', model.a_string
+ end
+
+ test "decorations occur in order of declaration" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :other) do |type|
+ StringDecorator.new(type, 'decorated again!')
+ end
+
+ model = Model.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated! decorated again!', model.a_string
+ end
+
+ test "decorating attributes does not modify parent classes" do
+ Model.attribute :another_string, Type::String.new, default: 'whatever'
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ child_class = Class.new(Model)
+ child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
+ child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello!')
+ child = child_class.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated!', model.a_string
+ assert_equal 'whatever', model.another_string
+ assert_equal 'Hello! decorated! decorated!', child.a_string
+ assert_equal 'whatever decorated!', child.another_string
+ end
+
+ class Multiplier < SimpleDelegator
+ def type_cast_from_user(value)
+ return if value.nil?
+ value * 2
+ end
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ test "decorating with a proc" do
+ Model.attribute :an_int, Type::Integer.new
+ type_is_integer = proc { |_, type| type.type == :integer }
+ Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
+ Multiplier.new(type)
+ end
+
+ model = Model.new(a_string: 'whatever', an_int: 1)
+
+ assert_equal 'whatever', model.a_string
+ assert_equal 2, model.an_int
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index c0659fddef..4741ee8799 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -12,6 +12,7 @@ module ActiveRecord
@klass = Class.new do
def self.superclass; Base; end
def self.base_class; self; end
+ def self.decorate_matching_attribute_types(*); end
include ActiveRecord::AttributeMethods
diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb
deleted file mode 100644
index 75de773961..0000000000
--- a/activerecord/test/cases/attribute_methods/serialization_test.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module AttributeMethods
- class SerializationTest < ActiveSupport::TestCase
- class FakeColumn < Struct.new(:name)
- def type; :integer; end
- def type_cast(s); "#{s}!"; end
- end
-
- class NullCoder
- def load(v); v; end
- end
-
- def test_type_cast_serialized_value
- value = Serialization::Attribute.new(NullCoder.new, "Hello world", :serialized)
- type = Serialization::Type.new(FakeColumn.new)
- assert_equal "Hello world!", type.type_cast(value)
- end
-
- def test_type_cast_unserialized_value
- value = Serialization::Attribute.new(nil, "Hello world", :unserialized)
- type = Serialization::Type.new(FakeColumn.new)
- type.type_cast(value)
- assert_equal "Hello world", type.type_cast(value)
- end
- end
- end
-end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 6c581a432f..ab67cf4085 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
@@ -143,7 +143,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase
# Syck calls respond_to? before actually calling initialize
def test_respond_to_with_allocated_object
- topic = Topic.allocate
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ end
+
+ topic = klass.allocate
assert !topic.respond_to?("nothingness")
assert !topic.respond_to?(:nothingness)
assert_respond_to topic, "title"
@@ -253,6 +257,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes
end
+ def test_attributes_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
+ assert_equal klass.column_names, klass.new.attributes.keys
+ assert_not klass.new.attributes.key?('id')
+ end
+
def test_hashes_not_mangled
new_topic = { :title => "New Topic" }
new_topic_values = { :title => "AnotherTopic" }
@@ -288,10 +301,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
@@ -299,6 +312,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
computer = Computer.select('id').first
assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = 'Hello!' }
+ assert_nothing_raised { computer[:developer] = 'Hello!' }
end
def test_read_attribute_when_false
@@ -358,10 +373,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
@@ -449,10 +464,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
+ topic = @target.new(:title => 'Budget')
meth = "title#{suffix}"
assert topic.respond_to?(meth)
@@ -463,10 +478,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
[['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
@target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
+ topic = @target.new(:title => 'Budget')
meth = "#{prefix}title#{suffix}"
assert topic.respond_to?(meth)
@@ -515,44 +530,36 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_only_time_related_columns_are_meant_to_be_cached_by_default
- expected = %w(datetime timestamp time date).sort
- assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
- end
+ def test_deprecated_cache_attributes
+ assert_deprecated do
+ Topic.cache_attributes :replies_count
+ end
- def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
- default_attributes = Topic.cached_attributes
- Topic.cache_attributes :replies_count
- expected = default_attributes + ["replies_count"]
- assert_equal expected.sort, Topic.cached_attributes.sort
- Topic.instance_variable_set "@cached_attributes", nil
- end
+ assert_deprecated do
+ Topic.cached_attributes
+ end
- def test_cacheable_columns_are_actually_cached
- assert_equal cached_columns.sort, Topic.cached_attributes.sort
+ assert_deprecated do
+ Topic.cache_attribute? :replies_count
+ end
end
- def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else
- t = topics(:first)
- cache = t.instance_variable_get "@attributes_cache"
+ def test_converted_values_are_returned_after_assignment
+ developer = Developer.new(name: 1337, salary: "50000")
- assert_not_nil cache
- assert cache.empty?
+ assert_equal "50000", developer.salary_before_type_cast
+ assert_equal 1337, developer.name_before_type_cast
- all_columns = Topic.columns.map(&:name)
- uncached_columns = all_columns - cached_columns
+ assert_equal 50000, developer.salary
+ assert_equal "1337", developer.name
- all_columns.each do |attr_name|
- attribute_gets_cached = Topic.cache_attribute?(attr_name)
- val = t.send attr_name unless attr_name == "type"
- if attribute_gets_cached
- assert cached_columns.include?(attr_name)
- assert_equal val, cache[attr_name]
- else
- assert uncached_columns.include?(attr_name)
- assert !cache.include?(attr_name)
- end
- end
+ 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
@@ -728,19 +735,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
- assert !@target.instance_method_already_implemented?(:title)
- topic = @target.new
- assert_nil topic.title
- Object.send(:undef_method, :title) # remove test method from object
+ 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 !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
@@ -804,6 +832,49 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal !real_topic.title?, klass.find(real_topic.id).title?
end
+ def test_calling_super_when_parent_does_not_define_method_raises_error
+ klass = new_topic_like_ar_class do
+ def some_method_that_is_not_on_super
+ super
+ end
+ end
+
+ assert_raise(NoMethodError) do
+ klass.new.some_method_that_is_not_on_super
+ end
+ end
+
+ def test_attribute_method?
+ assert @target.attribute_method?(:title)
+ assert @target.attribute_method?(:title=)
+ assert_not @target.attribute_method?(:wibble)
+ end
+
+ def test_attribute_method_returns_false_if_table_does_not_exist
+ @target.table_name = 'wibble'
+ assert_not @target.attribute_method?(:title)
+ end
+
+ def test_attribute_names_on_new_record
+ model = @target.new
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_on_queried_record
+ model = @target.last!
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_with_custom_select
+ model = @target.select('id').last!
+
+ assert_equal ['id'], model.attribute_names
+ # Sanity check, make sure other columns exist
+ assert_not_equal ['id'], @target.column_names
+ end
+
private
def new_topic_like_ar_class(&block)
@@ -817,7 +888,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def cached_columns
- Topic.columns.map(&:name) - Topic.serialized_attributes.keys
+ Topic.columns.map(&:name)
end
def time_related_columns_on_topic
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..dc20c3c676
--- /dev/null
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -0,0 +1,165 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeSetTest < ActiveRecord::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: '3.3')
+
+ assert_equal '3.3', attributes[:foo].value_before_type_cast
+ assert_equal nil, attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal 'foo', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert clone.frozen?
+ assert_not attributes.frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes[:foo].initialized?
+ assert_not attributes[:bar].initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.key?(:foo)
+ assert_not attributes.key?(:bar)
+ end
+
+ test "unknown attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.key?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble)
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
+ assert_equal 'bar!', value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value + " from database"
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: '1.1')
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
new file mode 100644
index 0000000000..91f6aee931
--- /dev/null
+++ b/activerecord/test/cases/attribute_test.rb
@@ -0,0 +1,142 @@
+require 'cases/helper'
+require 'minitest/mock'
+
+module ActiveRecord
+ class AttributeTest < ActiveRecord::TestCase
+ setup do
+ @type = Minitest::Mock.new
+ end
+
+ teardown do
+ assert @type.verify
+ end
+
+ test "from_database + read type casts from database" do
+ @type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from database', type_cast_value
+ end
+
+ test "from_user + read type casts from user" do
+ @type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
+ attribute = Attribute.from_user(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from user', type_cast_value
+ end
+
+ test "reading memoizes the value" do
+ @type.expect(:type_cast_from_database, 'from the database', ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_value = attribute.value
+ second_read = attribute.value
+
+ assert_equal 'from the database', type_cast_value
+ assert_same type_cast_value, second_read
+ end
+
+ test "reading memoizes falsy values" do
+ @type.expect(:type_cast_from_database, false, ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ attribute.value
+ attribute.value
+ end
+
+ test "read_before_typecast returns the given value" do
+ attribute = Attribute.from_database(nil, 'raw value', @type)
+
+ raw_value = attribute.value_before_type_cast
+
+ assert_equal 'raw value', raw_value
+ end
+
+ test "from_database + read_for_database type casts to and from database" do
+ @type.expect(:type_cast_from_database, 'read from database', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "from_user + read_for_database type casts from the user to the database" do
+ @type.expect(:type_cast_from_user, 'read from user', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
+ attribute = Attribute.from_user(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "duping dups the value" do
+ @type.expect(:type_cast_from_database, 'type cast', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ value_from_orig = attribute.value
+ value_from_clone = attribute.dup.value
+ value_from_orig << ' foo'
+
+ assert_equal 'type cast foo', value_from_orig
+ assert_equal 'type cast', value_from_clone
+ end
+
+ test "duping does not dup the value if it is not dupable" do
+ @type.expect(:type_cast_from_database, false, ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ assert_same attribute.value, attribute.dup.value
+ end
+
+ test "duping does not eagerly type cast if we have not yet type cast" do
+ attribute = Attribute.from_database(nil, 'a value', @type)
+ attribute.dup
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ value + " from database"
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
+ end
+end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
new file mode 100644
index 0000000000..79ef0502cb
--- /dev/null
+++ b/activerecord/test/cases/attributes_test.rb
@@ -0,0 +1,111 @@
+require 'cases/helper'
+
+class OverloadedType < ActiveRecord::Base
+ attribute :overloaded_float, Type::Integer.new
+ attribute :overloaded_string_with_limit, Type::String.new(limit: 50)
+ attribute :non_existent_decimal, Type::Decimal.new
+ attribute :string_with_default, Type::String.new, default: 'the overloaded default'
+end
+
+class ChildOfOverloadedType < OverloadedType
+end
+
+class GrandchildOfOverloadedType < ChildOfOverloadedType
+ attribute :overloaded_float, Type::Float.new
+end
+
+class UnoverloadedType < ActiveRecord::Base
+ self.table_name = 'overloaded_types'
+end
+
+module ActiveRecord
+ class CustomPropertiesTest < ActiveRecord::TestCase
+ def test_overloading_types
+ data = OverloadedType.new
+
+ data.overloaded_float = "1.1"
+ data.unoverloaded_float = "1.1"
+
+ assert_equal 1, data.overloaded_float
+ assert_equal 1.1, data.unoverloaded_float
+ end
+
+ def test_overloaded_properties_save
+ data = OverloadedType.new
+
+ data.overloaded_float = "2.2"
+ data.save!
+ data.reload
+
+ assert_equal 2, data.overloaded_float
+ assert_kind_of Fixnum, OverloadedType.last.overloaded_float
+ assert_equal 2.0, UnoverloadedType.last.overloaded_float
+ assert_kind_of Float, UnoverloadedType.last.overloaded_float
+ end
+
+ def test_properties_assigned_in_constructor
+ data = OverloadedType.new(overloaded_float: '3.3')
+
+ assert_equal 3, data.overloaded_float
+ end
+
+ def test_overloaded_properties_with_limit
+ assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ end
+
+ def test_nonexistent_attribute
+ data = OverloadedType.new(non_existent_decimal: 1)
+
+ assert_equal BigDecimal.new(1), data.non_existent_decimal
+ assert_raise ActiveRecord::UnknownAttributeError do
+ UnoverloadedType.new(non_existent_decimal: 1)
+ end
+ end
+
+ def test_changing_defaults
+ data = OverloadedType.new
+ unoverloaded_data = UnoverloadedType.new
+
+ assert_equal 'the overloaded default', data.string_with_default
+ assert_equal 'the original default', unoverloaded_data.string_with_default
+ end
+
+ def test_children_inherit_custom_properties
+ data = ChildOfOverloadedType.new(overloaded_float: '4.4')
+
+ assert_equal 4, data.overloaded_float
+ end
+
+ def test_children_can_override_parents
+ data = GrandchildOfOverloadedType.new(overloaded_float: '4.4')
+
+ assert_equal 4.4, data.overloaded_float
+ end
+
+ def test_overloading_properties_does_not_change_column_order
+ column_names = OverloadedType.column_names
+ assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names
+ end
+
+ def test_caches_are_cleared
+ klass = Class.new(OverloadedType)
+
+ assert_equal 6, klass.columns.length
+ assert_not klass.columns_hash.key?('wibble')
+ assert_equal 6, klass.column_types.length
+ assert_equal 6, klass.column_defaults.length
+ assert_not klass.column_names.include?('wibble')
+ assert_equal 5, klass.content_columns.length
+
+ klass.attribute :wibble, Type::Value.new
+
+ assert_equal 7, klass.columns.length
+ assert klass.columns_hash.key?('wibble')
+ assert_equal 7, klass.column_types.length
+ assert_equal 7, klass.column_defaults.length
+ assert klass.column_names.include?('wibble')
+ assert_equal 6, klass.content_columns.length
+ end
+ end
+end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index c55dd685a1..09892d50ba 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -21,6 +21,31 @@ 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
@@ -39,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
@@ -51,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
@@ -597,15 +618,15 @@ 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
- def teardown
+ teardown do
# We are running without transactional fixtures and need to cleanup.
Bird.delete_all
+ Parrot.delete_all
@ship.delete
@pirate.delete
end
@@ -662,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
+ @ship.pirate.catchphrase = "Changed Catchphrase"
+
assert_raise(RuntimeError) { assert !@pirate.save }
assert_not_nil @pirate.reload.ship
end
+ def test_should_save_changed_has_one_changed_object_if_child_is_saved
+ @pirate.ship.name = "NewName"
+ assert @pirate.save
+ assert_equal "NewName", @pirate.ship.reload.name
+ end
+
+ def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
+ @pirate.ship.expects(:save).never
+ assert @pirate.save
+ end
+
# belongs_to
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
assert !@ship.pirate.marked_for_destruction?
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 8a0b0b9589..4c0b0c868a 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -102,8 +102,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_columns_should_obey_set_primary_key
- pk = Subscriber.columns.find { |x| x.name == 'nick' }
- assert pk.primary, 'nick should be primary key'
+ pk = Subscriber.columns_hash[Subscriber.primary_key]
+ assert_equal 'nick', pk.name, 'nick should be primary key'
end
def test_primary_key_with_no_id
@@ -160,19 +160,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_date_objects
- if current_adapter?(:SybaseAdapter)
- # Sybase ctlib does not (yet?) support the date type; use datetime instead.
- assert_kind_of(
- Time, Topic.find(1).last_read,
- "The last_read attribute should be of the Time class"
- )
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_kind_of(
- Date, Topic.find(1).last_read,
- "The last_read attribute should be of the Date class"
- )
- end
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_kind_of(
+ Date, Topic.find(1).last_read,
+ "The last_read attribute should be of the Date class"
+ )
end
def test_previously_changed
@@ -212,7 +204,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
@@ -480,8 +472,8 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- # Oracle, and Sybase do not have a TIME datatype.
- unless current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ unless current_adapter?(:OracleAdapter)
def test_utc_as_time_zone
with_timezone_config default: :utc do
attributes = { "bonus_time" => "5:42:00AM" }
@@ -515,12 +507,7 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(topic.id)
assert_nil topic.last_read
- # Sybase adapter does not allow nulls in boolean columns
- if current_adapter?(:SybaseAdapter)
- assert topic.approved == false
- else
- assert_nil topic.approved
- end
+ assert_nil topic.approved
end
def test_equality
@@ -531,8 +518,13 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal Topic.find('1-meowmeow'), Topic.find(1)
end
+ def test_find_by_slug_with_array
+ assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2])
+ end
+
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
@@ -544,6 +536,47 @@ 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
@@ -578,12 +611,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal nil, Topic.find_by_id(topic.id)
end
- def test_blank_ids
- one = Subscriber.new(:id => '')
- two = Subscriber.new(:id => '')
- assert_equal one, two
- end
-
def test_comparison_with_different_objects
topic = Topic.create
category = Category.create(:name => "comparison")
@@ -649,8 +676,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attributes_on_dummy_time
- # Oracle, and Sybase do not have a TIME datatype.
- return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ return true if current_adapter?(:OracleAdapter)
with_timezone_config default: :local do
attributes = {
@@ -663,8 +690,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attributes_on_dummy_time_with_invalid_time
- # Oracle, and Sybase do not have a TIME datatype.
- return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ return true if current_adapter?(:OracleAdapter)
attributes = {
"bonus_time" => "not a time"
@@ -751,8 +778,14 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal("c", duped_topic.title)
end
+ DeveloperSalary = Struct.new(:amount)
def test_dup_with_aggregate_of_same_name_as_attribute
- dev = DeveloperWithAggregate.find(1)
+ developer_with_aggregate = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ composed_of :salary, :class_name => 'BasicsTest::DeveloperSalary', :mapping => [%w(salary amount)]
+ end
+
+ dev = developer_with_aggregate.find(1)
assert_kind_of DeveloperSalary, dev.salary
dup = nil
@@ -951,6 +984,10 @@ class BasicsTest < ActiveRecord::TestCase
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
+
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+ attribute :atoms_in_universe, Type::Integer.new
end
def test_big_decimal_conditions
@@ -1310,14 +1347,32 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError)
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError)
assert_raises NoMethodError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
end
+ def test_compute_type_on_undefined_method
+ error = nil
+ begin
+ Class.new(Author) do
+ alias_method :foo, :bar
+ end
+ rescue => e
+ error = e
+ end
+
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e)
+
+ exception = assert_raises NameError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ assert_equal error.message, exception.message
+ end
+
def test_compute_type_argument_error
- ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError)
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError)
assert_raises ArgumentError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
@@ -1451,15 +1506,14 @@ class BasicsTest < ActiveRecord::TestCase
attrs = topic.attributes.dup
attrs.delete 'id'
- typecast = Class.new {
+ typecast = Class.new(ActiveRecord::Type::Value) {
def type_cast value
"t.lo"
end
}
types = { 'author_name' => typecast.new }
- topic = Topic.allocate.init_with 'attributes' => attrs,
- 'column_types' => types
+ topic = Topic.instantiate(attrs, types)
assert_equal 't.lo', topic.author_name
end
@@ -1560,4 +1614,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal after_handler, new_handler
assert_equal orig_handler, klass.connection_handler
end
+
+ # Note: This is a performance optimization for Array#uniq and Hash#[] with
+ # AR::Base objects. If the future has made this irrelevant, feel free to
+ # delete this.
+ test "records without an id have unique hashes" do
+ assert_not_equal Post.new.hash, Post.new.hash
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 9a486cf8b8..ccf2be369d 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -2,9 +2,9 @@
require "cases/helper"
# Without using prepared statements, it makes no sense to test
-# BLOB data with DB2 or Firebird, because the length of a statement
+# BLOB data with DB2, because the length of a statement
# is limited to 32KB.
-unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
+unless current_adapter?(:DB2Adapter)
require 'models/binary'
class BinaryTest < ActiveRecord::TestCase
@@ -21,7 +21,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
name = binary.name
- # Mysql adapter doesn't properly encode things, so we have to do it
+ # MySQL adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
name.force_encoding(Encoding::UTF_8)
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 291751c435..0bc7ee6d64 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -20,13 +20,13 @@ module ActiveRecord
def setup
super
@connection = ActiveRecord::Base.connection
- @listener = LogListener.new
- @pk = Topic.columns.find { |c| c.primary }
- ActiveSupport::Notifications.subscribe('sql.active_record', @listener)
+ @subscriber = LogListener.new
+ @pk = Topic.columns_hash[Topic.primary_key]
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
end
- def teardown
- ActiveSupport::Notifications.unsubscribe(@listener)
+ teardown do
+ ActiveSupport::Notifications.unsubscribe(@subscription)
end
if ActiveRecord::Base.connection.supports_statement_cache?
@@ -37,7 +37,7 @@ module ActiveRecord
@connection.exec_query(sql, 'SQL', binds)
- message = @listener.calls.find { |args| args[4][:sql] == sql }
+ message = @subscriber.calls.find { |args| args[4][:sql] == sql }
assert_equal binds, message[4][:binds]
end
@@ -48,24 +48,22 @@ module ActiveRecord
@connection.exec_query(sql, 'SQL', binds)
- message = @listener.calls.find { |args| args[4][:sql] == sql }
+ 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 = @listener.calls.find { |args| args[4][:binds] == binds }
+ 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]]
+ :binds => [[@pk, 10]]
}
event = ActiveSupport::Notifications::Event.new(
'foo',
@@ -87,7 +85,7 @@ module ActiveRecord
}.new
logger.sql event
- assert_match([[pk.name, 10]].inspect, logger.debugs.first)
+ assert_match([[@pk.name, 10]].inspect, logger.debugs.first)
end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index db999f90ab..319ea9260a 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -15,6 +15,10 @@ Company.has_many :accounts
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
+
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+ attribute :atoms_in_universe, Type::Integer.new
end
class CalculationsTest < ActiveRecord::TestCase
@@ -49,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_nil NumericData.average(:bank_balance)
end
- def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
- assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg')
- assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg')
- end
-
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
@@ -387,6 +386,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)
@@ -592,4 +605,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [1,2,3,4,5], taks_relation.pluck(:id)
assert_equal [false, true, true, true, true], taks_relation.pluck(:approved)
end
+
+ def test_pluck_columns_with_same_name
+ expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]]
+ actual = Topic.joins(:replies)
+ .pluck('topics.title', 'replies_topics.title')
+ assert_equal expected, actual
+ end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index c7b64f29c3..bcfd66b4bf 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -11,30 +11,10 @@ module ActiveRecord
@viz = @adapter.schema_creation
end
- def test_can_set_coder
- column = Column.new("title", nil, "varchar(20)")
- column.coder = YAML
- assert_equal YAML, column.coder
- end
-
- def test_encoded?
- column = Column.new("title", nil, "varchar(20)")
- assert !column.encoded?
-
- column.coder = YAML
- assert column.encoded?
- end
-
- def test_type_case_coded_column
- column = Column.new("title", nil, "varchar(20)")
- column.coder = YAML
- assert_equal "hello", column.type_cast("--- hello")
- end
-
# Avoid column definitions in create table statements like:
# `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
- column = Column.new("title", nil, "varchar(20)")
+ column = Column.new("title", nil, Type::String.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -42,7 +22,7 @@ module ActiveRecord
end
def test_should_include_default_clause_when_default_is_present
- column = Column.new("title", "Hello", "varchar(20)")
+ column = Column.new("title", "Hello", Type::String.new(limit: 20))
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -50,7 +30,7 @@ module ActiveRecord
end
def test_should_specify_not_null_if_null_option_is_false
- column = Column.new("title", "Hello", "varchar(20)", false)
+ column = Column.new("title", "Hello", Type::String.new(limit: 20), "varchar(20)", false)
column_def = ColumnDefinition.new(
column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
@@ -59,81 +39,81 @@ module ActiveRecord
if current_adapter?(:MysqlAdapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)")
+ binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)")
+ varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "a", "blob")
+ MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob")
end
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "Hello", "text")
+ MysqlAdapter::Column.new("title", "Hello", Type::Text.new)
end
- text_column = MysqlAdapter::Column.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
assert_equal nil, text_column.default
- not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false)
+ not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = MysqlAdapter::Column.new("title", nil, "blob")
+ blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob")
assert !blob_column.has_default?
- text_column = MysqlAdapter::Column.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
assert !text_column.has_default?
end
end
if current_adapter?(:Mysql2Adapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)")
+ binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)")
+ varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "a", "blob")
+ Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob")
end
assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "Hello", "text")
+ Mysql2Adapter::Column.new("title", "Hello", Type::Text.new)
end
- text_column = Mysql2Adapter::Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
assert_equal nil, text_column.default
- not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false)
+ not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false)
assert_equal "", not_null_text_column.default
end
def test_has_default_should_return_false_for_blob_and_text_data_types
- blob_column = Mysql2Adapter::Column.new("title", nil, "blob")
+ blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob")
assert !blob_column.has_default?
- text_column = Mysql2Adapter::Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
assert !text_column.has_default?
end
end
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
deleted file mode 100644
index 2a6d8cc2ab..0000000000
--- a/activerecord/test/cases/column_test.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-require "cases/helper"
-require 'models/company'
-
-module ActiveRecord
- module ConnectionAdapters
- class ColumnTest < ActiveRecord::TestCase
- def test_type_cast_boolean
- column = Column.new("field", nil, "boolean")
- assert column.type_cast('').nil?
- assert column.type_cast(nil).nil?
-
- assert column.type_cast(true)
- assert column.type_cast(1)
- assert column.type_cast('1')
- assert column.type_cast('t')
- assert column.type_cast('T')
- assert column.type_cast('true')
- assert column.type_cast('TRUE')
- assert column.type_cast('on')
- assert column.type_cast('ON')
-
- # explicitly check for false vs nil
- assert_equal false, column.type_cast(false)
- assert_equal false, column.type_cast(0)
- assert_equal false, column.type_cast('0')
- assert_equal false, column.type_cast('f')
- assert_equal false, column.type_cast('F')
- assert_equal false, column.type_cast('false')
- assert_equal false, column.type_cast('FALSE')
- assert_equal false, column.type_cast('off')
- assert_equal false, column.type_cast('OFF')
- assert_equal false, column.type_cast(' ')
- assert_equal false, column.type_cast("\u3000\r\n")
- assert_equal false, column.type_cast("\u0000")
- assert_equal false, column.type_cast('SOMETHING RANDOM')
- end
-
- def test_type_cast_integer
- column = Column.new("field", nil, "integer")
- assert_equal 1, column.type_cast(1)
- assert_equal 1, column.type_cast('1')
- assert_equal 1, column.type_cast('1ignore')
- assert_equal 0, column.type_cast('bad1')
- assert_equal 0, column.type_cast('bad')
- assert_equal 1, column.type_cast(1.7)
- assert_equal 0, column.type_cast(false)
- assert_equal 1, column.type_cast(true)
- assert_nil column.type_cast(nil)
- end
-
- def test_type_cast_non_integer_to_integer
- column = Column.new("field", nil, "integer")
- assert_nil column.type_cast([1,2])
- assert_nil column.type_cast({1 => 2})
- assert_nil column.type_cast((1..2))
- end
-
- def test_type_cast_activerecord_to_integer
- column = Column.new("field", nil, "integer")
- firm = Firm.create(:name => 'Apple')
- assert_nil column.type_cast(firm)
- end
-
- def test_type_cast_object_without_to_i_to_integer
- column = Column.new("field", nil, "integer")
- assert_nil column.type_cast(Object.new)
- end
-
- def test_type_cast_nan_and_infinity_to_integer
- column = Column.new("field", nil, "integer")
- assert_nil column.type_cast(Float::NAN)
- assert_nil column.type_cast(1.0/0.0)
- end
-
- def test_type_cast_time
- column = Column.new("field", nil, "time")
- assert_equal nil, column.type_cast(nil)
- assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast('ABC')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, column.type_cast(time_string).strftime("%T")
- end
-
- def test_type_cast_datetime_and_timestamp
- [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column|
- assert_equal nil, column.type_cast(nil)
- assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
- assert_equal nil, column.type_cast('ABC')
-
- datetime_string = Time.now.utc.strftime("%FT%T")
- assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T")
- end
- end
-
- def test_type_cast_date
- column = Column.new("field", nil, "date")
- assert_equal nil, column.type_cast(nil)
- assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
- assert_equal nil, column.type_cast('ABC')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, column.type_cast(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- column = Column.new("field", nil, "integer")
- 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/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 318cc5a32c..3e33b30144 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -2,133 +2,6 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
-
- class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase
-
- def klass
- ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig
- end
-
- def setup
- @previous_database_url = ENV.delete("DATABASE_URL")
- end
-
- def teardown
- ENV["DATABASE_URL"] = @previous_database_url
- end
-
- def test_string_connection
- config = { "production" => "postgres://localhost/foo" }
- actual = klass.new(config).resolve
- expected = { "production" =>
- { "adapter" => "postgresql",
- "database" => "foo",
- "host" => "localhost"
- }
- }
- assert_equal expected, actual
- end
-
- def test_url_sub_key
- config = { "production" => { "url" => "postgres://localhost/foo" } }
- actual = klass.new(config).resolve
- expected = { "production" =>
- { "adapter" => "postgresql",
- "database" => "foo",
- "host" => "localhost"
- }
- }
- assert_equal expected, actual
- end
-
- def test_hash
- config = { "production" => { "adapter" => "postgres", "database" => "foo" } }
- actual = klass.new(config).resolve
- assert_equal config, actual
- end
-
- def test_blank
- config = {}
- actual = klass.new(config).resolve
- assert_equal config, actual
- end
-
- def test_blank_with_database_url
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
-
- config = {}
- actual = klass.new(config).resolve
- expected = { "adapter" => "postgresql",
- "database" => "foo",
- "host" => "localhost" }
- assert_equal expected, actual["production"]
- assert_equal expected, actual["development"]
- assert_equal expected, actual["test"]
- assert_equal nil, actual[:production]
- assert_equal nil, actual[:development]
- assert_equal nil, actual[:test]
- end
-
- def test_sting_with_database_url
- ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
-
- config = { "production" => "postgres://localhost/foo" }
- actual = klass.new(config).resolve
-
- expected = { "production" =>
- { "adapter" => "postgresql",
- "database" => "foo",
- "host" => "localhost"
- }
- }
- assert_equal expected, actual
- end
-
- def test_url_sub_key_with_database_url
- ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
-
- config = { "production" => { "url" => "postgres://localhost/foo" } }
- actual = klass.new(config).resolve
- expected = { "production" =>
- { "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 = {"production" => { "pool" => "5" } }
- actual = klass.new(config).resolve
- expected = { "production" =>
- { "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 = {"production" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } }
- actual = klass.new(config).resolve
- expected = { "production" =>
- { "adapter" => "postgresql",
- "database" => "foo",
- "host" => "localhost",
- "pool" => "5"
- }
- }
- assert_equal expected, actual
- end
- end
-
class ConnectionHandlerTest < ActiveRecord::TestCase
def setup
@klass = Class.new(Base) { def self.name; 'klass'; 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..e1b2804a18
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -0,0 +1,204 @@
+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_database_url_with_ipv6_host_and_port
+ ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "::1",
+ "port" => 5454 }
+ assert_equal expected, actual["default_env"]
+ 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_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
new file mode 100644
index 0000000000..d4d67487db
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -0,0 +1,61 @@
+require "cases/helper"
+
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+module ActiveRecord
+ module ConnectionAdapters
+ class MysqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_boolean_types
+ emulate_booleans(true) do
+ assert_lookup_type :boolean, 'tinyint(1)'
+ assert_lookup_type :boolean, 'TINYINT(1)'
+ end
+ end
+
+ def test_string_types
+ assert_lookup_type :string, "enum('one', 'two', 'three')"
+ assert_lookup_type :string, "ENUM('one', 'two', 'three')"
+ assert_lookup_type :string, "set('one', 'two', 'three')"
+ assert_lookup_type :string, "SET('one', 'two', 'three')"
+ end
+
+ def test_binary_types
+ assert_lookup_type :binary, 'bit'
+ assert_lookup_type :binary, 'BIT'
+ end
+
+ def test_integer_types
+ emulate_booleans(false) do
+ assert_lookup_type :integer, 'tinyint(1)'
+ assert_lookup_type :integer, 'TINYINT(1)'
+ assert_lookup_type :integer, 'year'
+ assert_lookup_type :integer, 'YEAR'
+ end
+ end
+
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.type_map.lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+
+ def emulate_booleans(value)
+ old_emulate_booleans = @connection.emulate_booleans
+ change_emulate_booleans(value)
+ yield
+ ensure
+ change_emulate_booleans(old_emulate_booleans)
+ end
+
+ def change_emulate_booleans(value)
+ @connection.emulate_booleans = value
+ @connection.clear_cache!
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index ecad7c942f..c7531f5418 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -45,8 +45,8 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 12, @cache.columns('posts').size
- assert_equal 12, @cache.columns_hash('posts').size
+ assert_equal 11, @cache.columns('posts').size
+ assert_equal 11, @cache.columns_hash('posts').size
assert @cache.tables('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
new file mode 100644
index 0000000000..d5c1dc1e5d
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -0,0 +1,101 @@
+require "cases/helper"
+
+unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup
+module ActiveRecord
+ module ConnectionAdapters
+ class TypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_boolean_types
+ assert_lookup_type :boolean, 'boolean'
+ assert_lookup_type :boolean, 'BOOLEAN'
+ end
+
+ def test_string_types
+ assert_lookup_type :string, 'char'
+ assert_lookup_type :string, 'varchar'
+ assert_lookup_type :string, 'VARCHAR'
+ assert_lookup_type :string, 'varchar(255)'
+ assert_lookup_type :string, 'character varying'
+ end
+
+ def test_binary_types
+ assert_lookup_type :binary, 'binary'
+ assert_lookup_type :binary, 'BINARY'
+ assert_lookup_type :binary, 'blob'
+ assert_lookup_type :binary, 'BLOB'
+ end
+
+ def test_text_types
+ assert_lookup_type :text, 'text'
+ assert_lookup_type :text, 'TEXT'
+ assert_lookup_type :text, 'clob'
+ assert_lookup_type :text, 'CLOB'
+ end
+
+ def test_date_types
+ assert_lookup_type :date, 'date'
+ assert_lookup_type :date, 'DATE'
+ end
+
+ def test_time_types
+ assert_lookup_type :time, 'time'
+ assert_lookup_type :time, 'TIME'
+ end
+
+ def test_datetime_types
+ assert_lookup_type :datetime, 'datetime'
+ assert_lookup_type :datetime, 'DATETIME'
+ assert_lookup_type :datetime, 'timestamp'
+ assert_lookup_type :datetime, 'TIMESTAMP'
+ end
+
+ def test_decimal_types
+ assert_lookup_type :decimal, 'decimal'
+ assert_lookup_type :decimal, 'decimal(2,8)'
+ assert_lookup_type :decimal, 'DECIMAL'
+ assert_lookup_type :decimal, 'numeric'
+ assert_lookup_type :decimal, 'numeric(2,8)'
+ assert_lookup_type :decimal, 'NUMERIC'
+ assert_lookup_type :decimal, 'number'
+ assert_lookup_type :decimal, 'number(2,8)'
+ assert_lookup_type :decimal, 'NUMBER'
+ end
+
+ def test_float_types
+ assert_lookup_type :float, 'float'
+ assert_lookup_type :float, 'FLOAT'
+ assert_lookup_type :float, 'double'
+ assert_lookup_type :float, 'DOUBLE'
+ end
+
+ def test_integer_types
+ assert_lookup_type :integer, 'integer'
+ assert_lookup_type :integer, 'INTEGER'
+ assert_lookup_type :integer, 'tinyint'
+ assert_lookup_type :integer, 'smallint'
+ assert_lookup_type :integer, 'bigint'
+ end
+
+ def test_decimal_without_scale
+ types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)}
+ types.each do |type|
+ cast_type = @connection.type_map.lookup(type)
+
+ assert_equal :decimal, cast_type.type
+ assert_equal 2, cast_type.type_cast_from_user(2.1)
+ end
+ end
+
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.type_map.lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 1cf215017b..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
@@ -125,7 +124,6 @@ module ActiveRecord
@pool.checkout
@pool.checkout
@pool.checkout
- @pool.dead_connection_timeout = 0
connections = @pool.connections.dup
@@ -135,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_threadsafe?; 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 fdd1914cba..3c2f5d4219 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -82,15 +82,34 @@ module ActiveRecord
assert_equal password, spec["password"]
end
- def test_url_host_db_for_sqlite3
- spec = resolve 'sqlite3://foo:bar@dburl:9000/foo_test'
+ def test_url_with_authority_for_sqlite3
+ spec = resolve 'sqlite3:///foo_test'
assert_equal('/foo_test', spec["database"])
end
- def test_url_host_memory_db_for_sqlite3
- spec = resolve 'sqlite3://foo:bar@dburl:9000/:memory:'
+ 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/core_test.rb b/activerecord/test/cases/core_test.rb
index 2a52bf574c..715d92af99 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -1,6 +1,8 @@
require 'cases/helper'
require 'models/person'
require 'models/topic'
+require 'pp'
+require 'active_support/core_ext/string/strip'
class NonExistentTable < ActiveRecord::Base; end
@@ -30,4 +32,70 @@ class CoreTest < ActiveRecord::TestCase
def test_inspect_class_without_table
assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
end
+
+ def test_pretty_print_new
+ topic = Topic.new
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = <<-PRETTY.strip_heredoc
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
+ PRETTY
+ assert actual.start_with?(expected.split('XXXXXX').first)
+ assert actual.end_with?(expected.split('XXXXXX').last)
+ end
+
+ def test_pretty_print_persisted
+ topic = topics(:first)
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = <<-PRETTY.strip_heredoc
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
+ PRETTY
+ assert_match(/\A#{expected}\z/, actual)
+ end
+
+ def test_pretty_print_uninitialized
+ topic = Topic.allocate
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = "#<Topic:XXXXXX not initialized>\n"
+ assert actual.start_with?(expected.split('XXXXXX').first)
+ assert actual.end_with?(expected.split('XXXXXX').last)
+ end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index ee3d8a81c2..07a182070b 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase
class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id'
+ has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply'
end
class ::SpecialReply < ::Reply
@@ -51,6 +52,16 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test "reset counters by counter name" do
+ # throw the count off by 1
+ Topic.increment_counter(:replies_count, @topic.id)
+
+ # check that it gets reset
+ assert_difference '@topic.reload.replies_count', -1 do
+ Topic.reset_counters(@topic.id, :replies_count)
+ end
+ end
+
test 'reset multiple counters' do
Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1
assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do
@@ -154,10 +165,19 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
- test "the passed symbol needs to be an association name" do
+ test "the passed symbol needs to be an association name or counter name" do
e = assert_raises(ArgumentError) do
- Topic.reset_counters(@topic.id, :replies_count)
+ Topic.reset_counters(@topic.id, :undefined_count)
+ end
+ assert_equal "'Topic' has no association called 'undefined_count'", e.message
+ end
+
+ test "reset counter works with select declared on association" do
+ special = SpecialTopic.create!(:title => 'Special')
+ SpecialTopic.increment_counter(:replies_count, special.id)
+
+ assert_difference 'special.reload.replies_count', -1 do
+ SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
end
- assert_equal "'Topic' has no association called 'replies_count'", e.message
end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 7e3d91e08c..c089e63128 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -18,7 +18,7 @@ class DefaultTest < ActiveRecord::TestCase
end
end
- if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
+ if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
@@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
t.column :omit, :integer, :null => false
end
- assert_equal 0, klass.columns_hash['zero'].default
+ assert_equal '0', klass.columns_hash['zero'].default
assert !klass.columns_hash['zero'].null
# 0 in MySQL 4, nil in 5.
assert [0, nil].include?(klass.columns_hash['omit'].default)
@@ -206,7 +206,12 @@ 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
+ def test_default_containing_quote_and_colons
+ @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'"
+ assert_equal "foo'::bar", Default.new.string_col
+ end
+
+ 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 df4183c065..69a7f25213 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -169,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(:catchphrase => 'Yar!')
pirate.catchphrase = 'Ahoy!'
- pirate.reset_catchphrase!
+ assert_deprecated do
+ pirate.reset_catchphrase!
+ end
+ assert_equal "Yar!", pirate.catchphrase
+ assert_equal Hash.new, pirate.changes
+ assert !pirate.catchphrase_changed?
+ end
+
+ def test_restore_attribute!
+ pirate = Pirate.create!(:catchphrase => 'Yar!')
+ pirate.catchphrase = 'Ahoy!'
+
+ pirate.restore_catchphrase!
assert_equal "Yar!", pirate.catchphrase
assert_equal Hash.new, pirate.changes
assert !pirate.catchphrase_changed?
@@ -309,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_will_change!
pirate = Pirate.create!(:catchphrase => 'arr')
- pirate.catchphrase << ' matey'
assert !pirate.catchphrase_changed?
-
assert pirate.catchphrase_will_change!
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr'], pirate.catchphrase_change
- pirate.catchphrase << '!'
+ pirate.catchphrase << ' matey!'
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change
end
def test_association_assignment_changes_foreign_key
@@ -400,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_dup_objects_should_not_copy_dirty_flag_from_creator
pirate = Pirate.create!(:catchphrase => "shiver me timbers")
pirate_dup = pirate.dup
- pirate_dup.reset_catchphrase!
+ pirate_dup.restore_catchphrase!
pirate.catchphrase = "I love Rum"
assert pirate.catchphrase_changed?
assert !pirate_dup.catchphrase_changed?
@@ -445,11 +455,20 @@ class DirtyTest < ActiveRecord::TestCase
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
+
+ assert_not topic.changed?
+
topic.content[:b] = "b"
- #assert topic.changed? # Known bug, will fail
+
+ assert topic.changed?
+
topic.save!
+
+ assert_not topic.changed?
assert_equal "b", topic.content[:b]
+
topic.reload
+
assert_equal "b", topic.content[:b]
end
end
@@ -616,6 +635,32 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ test "defaults with type that implements `type_cast_for_database`" do
+ type = Class.new(ActiveRecord::Type::Value) do
+ def type_cast(value)
+ value.to_i
+ end
+
+ def type_cast_for_database(value)
+ value.to_s
+ end
+ end
+
+ model_class = Class.new(ActiveRecord::Base) do
+ self.table_name = 'numeric_data'
+ attribute :foo, type.new, default: 1
+ end
+
+ model = model_class.new
+ assert_not model.foo_changed?
+
+ model = model_class.new(foo: 1)
+ assert_not model.foo_changed?
+
+ model = model_class.new(foo: '1')
+ assert_not model.foo_changed?
+ 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 9e268dad74..94447addc1 100644
--- a/activerecord/test/cases/disconnected_test.rb
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -10,7 +10,7 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
end
- def teardown
+ teardown do
return if in_memory_db?
spec = ActiveRecord::Base.connection_config
ActiveRecord::Base.establish_connection(spec)
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 1e6ccecfab..638cffe0e6 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
@@ -132,5 +141,17 @@ module ActiveRecord
ensure
Topic.default_scopes = prev_default_scopes
end
+
+ def test_dup_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'parrots_pirates'
+ end
+
+ record = klass.create!
+
+ assert_nothing_raised do
+ record.dup
+ end
+ end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index 1b95708cb3..3b2f0dfe07 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -194,6 +194,7 @@ class EnumTest < ActiveRecord::TestCase
: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|
@@ -222,4 +223,67 @@ class EnumTest < ActiveRecord::TestCase
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_test.rb b/activerecord/test/cases/finder_test.rb
index b1eded6494..40e51a0cdc 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -33,6 +33,23 @@ class FinderTest < ActiveRecord::TestCase
assert_equal(topics(:first).title, Topic.find(1).title)
end
+ def test_find_with_proc_parameter_and_block
+ exception = assert_raises(RuntimeError) do
+ Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" }
+ end
+ assert_equal "should happen", exception.message
+
+ assert_nothing_raised(RuntimeError) do
+ Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title }
+ end
+ 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
@@ -56,17 +73,33 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Topic.exists?(id: [1, 9999])
assert_equal false, Topic.exists?(45)
- assert_equal false, Topic.exists?(Topic.new)
+ assert_equal false, Topic.exists?(Topic.new.id)
- begin
- assert_equal false, Topic.exists?("foo")
- rescue ActiveRecord::StatementInvalid
- # PostgreSQL complains about string comparison with integer field
- rescue Exception
- flunk
+ 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
- assert_raise(NoMethodError) { Topic.exists?([1,2]) }
+ 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
@@ -111,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- 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?
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 37c6af74da..042fdaf0bb 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -84,12 +84,6 @@ 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"])
@@ -254,7 +248,7 @@ class FixturesTest < ActiveRecord::TestCase
def test_fixtures_are_set_up_with_database_env_variable
db_url_tmp = ENV['DATABASE_URL']
- ENV['DATABASE_URL'] = "sqlite3:///:memory:"
+ ENV['DATABASE_URL'] = "sqlite3::memory:"
ActiveRecord::Base.stubs(:configurations).returns({})
test_case = Class.new(ActiveRecord::TestCase) do
fixtures :accounts
@@ -677,6 +671,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"))
@@ -689,6 +689,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)
@@ -782,6 +785,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/helper.rb b/activerecord/test/cases/helper.rb
index 3758224b0c..6a8aff4b69 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -9,6 +9,7 @@ require 'active_record'
require 'cases/test_case'
require 'active_support/dependencies'
require 'active_support/logger'
+require 'active_support/core_ext/string/strip'
require 'support/config'
require 'support/connection'
@@ -41,6 +42,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
@@ -106,6 +112,15 @@ def verify_default_timezone_config
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']
module ActiveRecord::TestFixtures::ClassMethods
def try_to_load_dependency_with_silence(*args)
@@ -163,7 +178,7 @@ class SQLSubscriber
def start(name, id, payload)
@payloads << payload
- @logged << [payload[:sql], payload[:name], payload[:binds]]
+ @logged << [payload[:sql].squish, payload[:name], payload[:binds]]
end
def finish(name, id, payload); end
@@ -184,3 +199,5 @@ module InTimeZone
ActiveRecord::Base.time_zone_aware_attributes = old_tz
end
end
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index 367d04a154..b4617cf6f9 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -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 e2ff2aa451..792950d24d 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -95,16 +95,8 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_a_bad_type_column
- #SQLServer need to turn Identity Insert On before manually inserting into the Identity column
- if current_adapter?(:SybaseAdapter)
- Company.connection.execute "SET IDENTITY_INSERT companies ON"
- end
Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')"
- #We then need to turn it back Off before continuing.
- if current_adapter?(:SybaseAdapter)
- Company.connection.execute "SET IDENTITY_INSERT companies OFF"
- end
assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) }
end
@@ -339,7 +331,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
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
index f6774d7ef4..8416c81f45 100644
--- a/activerecord/test/cases/invalid_connection_test.rb
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -12,7 +12,7 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist'
end
- def teardown
+ teardown do
Bird.remove_connection
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index debacf815c..285172d33e 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -122,7 +122,7 @@ module ActiveRecord
end
end
- def teardown
+ teardown do
%w[horses new_horses].each do |table|
if ActiveRecord::Base.connection.table_exists?(table)
ActiveRecord::Base.connection.drop_table(table)
@@ -271,16 +271,19 @@ module ActiveRecord
ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ''
end
- 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"
+ # 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
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index c373dc1511..0c9dff2c25 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -273,8 +273,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase
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 }
+ def test_yaml_dumping_with_lock_column
+ t1 = LockWithoutDefault.new
+ t2 = YAML.load(YAML.dump(t1))
+
+ assert_equal t1.attributes, t2.attributes
end
end
@@ -339,8 +342,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
def add_counter_column_to(model, col='test_count')
model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
model.reset_column_information
- # OpenBase does not set a value to existing rows when adding a not null default column
- model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
end
def remove_counter_column_from(model, col = :test_count)
@@ -367,7 +368,7 @@ end
# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
# blocks, so separate script called by Kernel#system is needed.
# (See exec vs. async_exec in the PostgreSQL adapter.)
-unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
+unless in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :people, :readers
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 97c0350911..a578e81844 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -125,5 +125,12 @@ class LogSubscriberTest < ActiveRecord::TestCase
wait
assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
end
+
+ 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 294f2eb9fe..c66eaf1ee1 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -11,10 +11,10 @@ module ActiveRecord
@table_name = :testings
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
+ ActiveRecord::Base.clear_cache!
end
def test_create_table_without_id
@@ -68,9 +68,9 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.default
- assert_equal false, three.default
- assert_equal 1, four.default
+ assert_equal true, two.type_cast_from_database(two.default)
+ assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
@@ -205,9 +205,9 @@ module ActiveRecord
connection.create_table table_name
end
- # Sybase, and SQLite3 will not allow you to add a NOT NULL
+ # SQLite3 will not allow you to add a NOT NULL
# column to a table without a default value.
- unless current_adapter?(:SybaseAdapter, :SQLite3Adapter)
+ unless current_adapter?(:SQLite3Adapter)
def test_add_column_not_null_without_default
connection.create_table :testings do |t|
t.column :foo, :string
@@ -226,18 +226,28 @@ module ActiveRecord
end
con = connection
- connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter)
connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
- connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
assert_nothing_raised {connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
assert_raises(ActiveRecord::StatementInvalid) do
- unless current_adapter?(:OpenBaseAdapter)
- connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
- else
- connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)",
- "Testing Insert","id",2)
- end
+ connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
+ end
+ end
+
+ def test_add_column_with_timestamp_type
+ connection.create_table :testings do |t|
+ t.column :foo, :timestamp
+ end
+
+ klass = Class.new(ActiveRecord::Base)
+ klass.table_name = 'testings'
+
+ assert_equal :datetime, klass.columns_hash['foo'].type
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_equal 'timestamp without time zone', klass.columns_hash['foo'].sql_type
+ else
+ assert_equal klass.connection.type_to_sql('datetime'), klass.columns_hash['foo'].sql_type
end
end
@@ -265,7 +275,7 @@ module ActiveRecord
person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
person_klass.reset_column_information
- assert_equal 99, person_klass.columns_hash["wealth"].default
+ assert_equal 99, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
@@ -277,20 +287,20 @@ module ActiveRecord
# change column default to see that column doesn't lose its not null definition
person_klass.connection.change_column_default "testings", "wealth", 100
person_klass.reset_column_information
- assert_equal 100, person_klass.columns_hash["wealth"].default
+ assert_equal 100, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# rename column to see that column doesn't lose its not null and/or default definition
person_klass.connection.rename_column "testings", "wealth", "money"
person_klass.reset_column_information
assert_nil person_klass.columns_hash["wealth"]
- assert_equal 100, person_klass.columns_hash["money"].default
+ assert_equal 100, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column
person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
person_klass.reset_column_information
- assert_equal 1000, person_klass.columns_hash["money"].default
+ assert_equal 1000, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column, make it nullable and clear default
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index c1d7cd5874..3e9d957ed3 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -8,7 +8,7 @@ module ActiveRecord
@connection = Minitest::Mock.new
end
- def teardown
+ teardown do
assert @connection.verify
end
@@ -72,6 +72,20 @@ module ActiveRecord
end
end
+ def test_references_column_type_with_polymorphic_and_type
+ with_change_table do |t|
+ @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string]
+ t.references :taggable, polymorphic: true, type: :string
+ end
+ end
+
+ def test_remove_references_column_type_with_polymorphic_and_type
+ with_change_table do |t|
+ @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string]
+ t.remove_references :taggable, polymorphic: true, type: :string
+ end
+ end
+
def test_timestamps_creates_updated_at_and_created_at
with_change_table do |t|
@connection.expect :add_timestamps, nil, [:delete_me]
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index ccf19fb4d0..763aa88f72 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -35,6 +35,14 @@ module ActiveRecord
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_nil TestModel.columns_hash["description"].limit
+ end
+
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_unabstracted_database_dependent_types
add_column :test_models, :intelligence_quotient, :tinyint
@@ -43,46 +51,46 @@ module ActiveRecord
end
end
- # We specifically do a manual INSERT here, and then test only the SELECT
- # functionality. This allows us to more easily catch INSERT being broken,
- # but SELECT actually working fine.
- def test_native_decimal_insert_manual_vs_automatic
- correct_value = '0012345678901234567890.0123456789'.to_d
-
- connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
-
- # Do a manual insertion
- if current_adapter?(:OracleAdapter)
- connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
- elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings
- connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
- elsif current_adapter?(:PostgreSQLAdapter)
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
- else
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
- end
+ unless current_adapter?(:SQLite3Adapter)
+ # We specifically do a manual INSERT here, and then test only the SELECT
+ # functionality. This allows us to more easily catch INSERT being broken,
+ # but SELECT actually working fine.
+ def test_native_decimal_insert_manual_vs_automatic
+ correct_value = '0012345678901234567890.0123456789'.to_d
+
+ connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+
+ # Do a manual insertion
+ if current_adapter?(:OracleAdapter)
+ connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
+ elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings
+ connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
+ elsif current_adapter?(:PostgreSQLAdapter)
+ connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ else
+ connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ end
- # SELECT
- row = TestModel.first
- assert_kind_of BigDecimal, row.wealth
+ # SELECT
+ row = TestModel.first
+ assert_kind_of BigDecimal, row.wealth
- # If this assert fails, that means the SELECT is broken!
- unless current_adapter?(:SQLite3Adapter)
- assert_equal correct_value, row.wealth
- end
+ # If this assert fails, that means the SELECT is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
- # Reset to old state
- TestModel.delete_all
+ # Reset to old state
+ TestModel.delete_all
- # Now use the Rails insertion
- TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
+ # Now use the Rails insertion
+ TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
- # SELECT
- row = TestModel.first
- assert_kind_of BigDecimal, row.wealth
+ # SELECT
+ row = TestModel.first
+ assert_kind_of BigDecimal, row.wealth
- # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
- unless current_adapter?(:SQLite3Adapter)
+ # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
assert_equal correct_value, row.wealth
end
end
@@ -113,54 +121,54 @@ module ActiveRecord
end
end
- def test_native_types
- add_column "test_models", "first_name", :string
- add_column "test_models", "last_name", :string
- add_column "test_models", "bio", :text
- add_column "test_models", "age", :integer
- add_column "test_models", "height", :float
- add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
- add_column "test_models", "birthday", :datetime
- add_column "test_models", "favorite_day", :date
- add_column "test_models", "moment_of_truth", :datetime
- add_column "test_models", "male", :boolean
-
- TestModel.create :first_name => 'bob', :last_name => 'bobsen',
- :bio => "I was born ....", :age => 18, :height => 1.78,
- :wealth => BigDecimal.new("12345678901234567890.0123456789"),
- :birthday => 18.years.ago, :favorite_day => 10.days.ago,
- :moment_of_truth => "1782-10-10 21:40:18", :male => true
-
- bob = TestModel.first
- assert_equal 'bob', bob.first_name
- assert_equal 'bobsen', bob.last_name
- assert_equal "I was born ....", bob.bio
- assert_equal 18, bob.age
-
- # Test for 30 significant digits (beyond the 16 of float), 10 of them
- # after the decimal place.
-
- unless current_adapter?(:SQLite3Adapter)
+ unless current_adapter?(:SQLite3Adapter)
+ def test_native_types
+ add_column "test_models", "first_name", :string
+ add_column "test_models", "last_name", :string
+ add_column "test_models", "bio", :text
+ add_column "test_models", "age", :integer
+ add_column "test_models", "height", :float
+ add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+ add_column "test_models", "birthday", :datetime
+ add_column "test_models", "favorite_day", :date
+ add_column "test_models", "moment_of_truth", :datetime
+ add_column "test_models", "male", :boolean
+
+ TestModel.create :first_name => 'bob', :last_name => 'bobsen',
+ :bio => "I was born ....", :age => 18, :height => 1.78,
+ :wealth => BigDecimal.new("12345678901234567890.0123456789"),
+ :birthday => 18.years.ago, :favorite_day => 10.days.ago,
+ :moment_of_truth => "1782-10-10 21:40:18", :male => true
+
+ bob = TestModel.first
+ assert_equal 'bob', bob.first_name
+ assert_equal 'bobsen', bob.last_name
+ assert_equal "I was born ....", bob.bio
+ assert_equal 18, bob.age
+
+ # Test for 30 significant digits (beyond the 16 of float), 10 of them
+ # after the decimal place.
+
assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
- end
- assert_equal true, bob.male?
+ assert_equal true, bob.male?
- assert_equal String, bob.first_name.class
- assert_equal String, bob.last_name.class
- assert_equal String, bob.bio.class
- assert_equal Fixnum, bob.age.class
- assert_equal Time, bob.birthday.class
+ assert_equal String, bob.first_name.class
+ assert_equal String, bob.last_name.class
+ assert_equal String, bob.bio.class
+ assert_equal Fixnum, bob.age.class
+ assert_equal Time, bob.birthday.class
- if current_adapter?(:OracleAdapter, :SybaseAdapter)
- # Sybase, and Oracle don't differentiate between date/time
- assert_equal Time, bob.favorite_day.class
- else
- assert_equal Date, bob.favorite_day.class
- end
+ if current_adapter?(:OracleAdapter)
+ # Oracle doesn't differentiate between date/time
+ assert_equal Time, bob.favorite_day.class
+ else
+ assert_equal Date, bob.favorite_day.class
+ end
- assert_instance_of TrueClass, bob.male?
- assert_kind_of BigDecimal, bob.wealth
+ assert_instance_of TrueClass, bob.male?
+ assert_kind_of BigDecimal, bob.wealth
+ end
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 87e29e41ba..77a752f050 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -18,8 +18,7 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 2d7a7ec73a..4e6d7963aa 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -53,13 +53,13 @@ module ActiveRecord
add_column 'test_models', 'salary', :integer, :default => 70000
default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default
- assert_equal 70000, default_before
+ assert_equal '70000', default_before
rename_column "test_models", "salary", "annual_salary"
assert TestModel.column_names.include?("annual_salary")
default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default
- assert_equal 70000, default_after
+ assert_equal '70000', default_after
end
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
@@ -193,14 +193,21 @@ module ActiveRecord
old_columns = connection.columns(TestModel.table_name)
assert old_columns.find { |c|
- c.name == 'approved' && c.type == :boolean && c.default == true
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' && c.type == :boolean && default == true
}
change_column :test_models, :approved, :boolean, :default => false
new_columns = connection.columns(TestModel.table_name)
- assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true }
- assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false }
+ assert_not new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == true
+ }
+ assert new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == false
+ }
change_column :test_models, :approved, :boolean, :default => true
end
@@ -274,6 +281,16 @@ module ActiveRecord
ensure
connection.drop_table(:my_table) rescue nil
end
+
+ def test_column_with_index
+ connection.create_table "my_table", force: true do |t|
+ t.string :item_number, index: true
+ end
+
+ assert connection.index_exists?("my_table", :item_number, name: :index_my_table_on_item_number)
+ ensure
+ connection.drop_table(:my_table) rescue nil
+ 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 a925cf4c05..e955beae1a 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -157,6 +157,23 @@ module ActiveRecord
assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
end
+ def test_invert_change_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column, [:table, :column, :type, {}]
+ end
+ end
+
+ def test_invert_change_column_default
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_default, [:table, :column, 'default_value']
+ end
+ end
+
+ def test_invert_change_column_null
+ add = @recorder.inverse_of :change_column_null, [:table, :column, true]
+ assert_equal [:change_column_null, [:table, :column, false]], add
+ end
+
def test_invert_remove_column
add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
assert_equal [:add_column, [:table, :column, :type, {}], nil], add
@@ -253,6 +270,31 @@ module ActiveRecord
enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
end
+
+ def test_invert_add_foreign_key
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people]
+ assert_equal [:remove_foreign_key, [:dogs, :people]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"]
+ assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column_and_name
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
+ assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable
+ end
+
+ def test_remove_foreign_key_is_irreversible
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
+ end
+
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"]
+ end
+ 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..bea9d6b2c9 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
@@ -120,6 +119,30 @@ module ActiveRecord
assert !connection.tables.include?('artists_musics')
end
+
+ def test_create_and_drop_join_table_with_common_prefix
+ with_table_cleanup do
+ connection.create_join_table 'audio_artists', 'audio_musics'
+ assert_includes connection.tables, 'audio_artists_musics'
+
+ connection.drop_join_table 'audio_artists', 'audio_musics'
+ assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't"
+ end
+ end
+
+ private
+
+ def with_table_cleanup
+ tables_before = connection.tables
+
+ yield
+ ensure
+ tables_after = connection.tables - tables_before
+
+ tables_after.each do |table|
+ connection.execute "DROP TABLE #{table}"
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
new file mode 100644
index 0000000000..c985092b4c
--- /dev/null
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -0,0 +1,242 @@
+require 'cases/helper'
+require 'support/ddl_helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+module ActiveRecord
+ class Migration
+ class ForeignKeyTest < ActiveRecord::TestCase
+ include DdlHelper
+ include SchemaDumpingHelper
+
+ class Rocket < ActiveRecord::Base
+ end
+
+ class Astronaut < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets" do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts" do |t|
+ t.string :name
+ t.references :rocket
+ end
+ end
+
+ teardown do
+ if defined?(@connection)
+ @connection.execute "DROP TABLE IF EXISTS astronauts"
+ @connection.execute "DROP TABLE IF EXISTS rockets"
+ end
+ end
+
+ def test_foreign_keys
+ foreign_keys = @connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name
+ end
+
+ def test_add_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_non_standard_primary_key
+ with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do
+ @connection.add_foreign_key(:astronauts, :space_shuttles,
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "space_shuttles", fk.to_table
+ assert_equal "pk", fk.primary_key
+
+ @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ end
+ end
+
+ def test_add_on_delete_restrict_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ # ON DELETE RESTRICT is the default on MySQL
+ assert_equal nil, fk.on_delete
+ else
+ assert_equal :restrict, fk.on_delete
+ end
+ end
+
+ def test_add_on_delete_cascade_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :cascade, fk.on_delete
+ end
+
+ def test_add_on_delete_nullify_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_delete
+ end
+
+ def test_on_update_and_on_delete_raises_with_invalid_values
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ end
+
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ end
+ end
+
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
+ end
+
+ def test_remove_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, :rockets
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: "rocket_id"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_schema_dumping
+ @connection.add_foreign_key :astronauts, :rockets
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+ end
+
+ def test_schema_dumping_with_options
+ output = dump_table_schema "fk_test_has_fk"
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
+
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
+
+ class CreateCitiesAndHousesMigration < ActiveRecord::Migration
+ def change
+ create_table("cities") { |t| }
+
+ create_table("houses") do |t|
+ t.column :city_id, :integer
+ end
+ add_foreign_key :houses, :cities, column: "city_id"
+ end
+ end
+
+ def test_add_foreign_key_is_reversible
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("houses").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
+ end
+ end
+end
+else
+module ActiveRecord
+ class Migration
+ class NoForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_add_foreign_key_should_be_noop
+ @connection.add_foreign_key :clubs, :categories
+ end
+
+ def test_remove_foreign_key_should_be_noop
+ @connection.remove_foreign_key :clubs, :categories
+ end
+
+ def test_foreign_keys_should_raise_not_implemented
+ assert_raises NotImplementedError do
+ @connection.foreign_keys("clubs")
+ end
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 8d1daa0a04..ac932378fd 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -21,34 +21,30 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
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_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')
- # 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
+ # 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
- def test_double_add_index
+ def test_double_add_index
+ connection.add_index(table_name, [:foo], :name => 'some_idx')
+ assert_raises(ArgumentError) {
connection.add_index(table_name, [:foo], :name => 'some_idx')
- assert_raises(ArgumentError) {
- connection.add_index(table_name, [:foo], :name => 'some_idx')
- }
- end
+ }
+ end
- 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
+ def test_remove_nonexistent_index
+ assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
end
def test_add_index_works_with_long_index_names
@@ -99,6 +95,12 @@ module ActiveRecord
assert connection.index_exists?(:testings, [:foo, :bar])
end
+ def test_index_exists_with_custom_name_checks_columns
+ connection.add_index :testings, [:foo, :bar], name: "my_index"
+ assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index")
+ assert_not connection.index_exists?(:testings, [:foo], name: "my_index")
+ end
+
def test_valid_index_options
assert_raise ArgumentError do
connection.add_index :testings, :foo, unqiue: true
@@ -127,50 +129,37 @@ module ActiveRecord
connection.add_index("testings", "last_name")
connection.remove_index("testings", "last_name")
- # Orcl nds shrt indx nms. Sybs 2.
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", :column => ["last_name", "first_name"])
+
+ # Oracle adapter cannot have specified index name larger than 30 characters
+ # Oracle adapter is shortening index name when just column list is given
+ unless current_adapter?(:OracleAdapter)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :column => ["last_name", "first_name"])
-
- # Oracle adapter cannot have specified index name larger than 30 characters
- # Oracle adapter is shortening index name when just column list is given
- unless current_adapter?(:OracleAdapter)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", "last_name_and_first_name")
- end
+ connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", "last_name_and_first_name")
+ end
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name"], :length => 10)
- connection.remove_index("testings", "last_name")
+ connection.add_index("testings", ["last_name"], :length => 10)
+ connection.remove_index("testings", "last_name")
- connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
- connection.remove_index("testings", ["last_name"])
+ connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
+ connection.remove_index("testings", ["last_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => 10)
- connection.remove_index("testings", ["last_name", "first_name"])
+ connection.add_index("testings", ["last_name", "first_name"], :length => 10)
+ connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
- connection.remove_index("testings", ["last_name", "first_name"])
- end
+ connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
+ connection.remove_index("testings", ["last_name", "first_name"])
- # quoting
- # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:OpenBaseAdapter)
- connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
- connection.remove_index("testings", :name => "key_idx", :unique => true)
- end
+ connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
+ connection.remove_index("testings", :name => "key_idx", :unique => true)
- # Sybase adapter does not support indexes on :boolean columns
- # OpenBase does not have named indexes. You must specify a single column
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
- connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
- connection.remove_index("testings", :name => "named_admin")
- end
+ connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
+ connection.remove_index("testings", :name => "named_admin")
# Selected adapters support index sort order
if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index 97efb94b66..319d3e1af3 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -3,7 +3,7 @@ require "cases/helper"
module ActiveRecord
class Migration
class LoggerTest < ActiveRecord::TestCase
- # mysql can't roll back ddl changes
+ # MySQL can't roll back ddl changes
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
@@ -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/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
new file mode 100644
index 0000000000..7afac83bd2
--- /dev/null
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -0,0 +1,53 @@
+require 'cases/helper'
+require "minitest/mock"
+
+module ActiveRecord
+ class Migration
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ def setup
+ super
+ @connection = Minitest::Mock.new
+ @app = Minitest::Mock.new
+ conn = @connection
+ @pending = Class.new(CheckPending) {
+ define_method(:connection) { conn }
+ }.new(@app)
+ @pending.instance_variable_set :@last_check, -1 # Force checking
+ end
+
+ def teardown
+ assert @connection.verify
+ assert @app.verify
+ super
+ end
+
+ def test_errors_if_pending
+ @connection.expect :supports_migrations?, true
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ assert_raise ActiveRecord::PendingMigrationError do
+ @pending.call(nil)
+ end
+ end
+ end
+
+ def test_checks_if_supported
+ @connection.expect :supports_migrations?, true
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, false do
+ @pending.call(:foo)
+ end
+ end
+
+ def test_doesnt_check_if_unsupported
+ @connection.expect :supports_migrations?, false
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ @pending.call(:foo)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 19eb7d3c9e..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
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index e9545f2cce..b8b4fa1135 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -55,6 +55,11 @@ module ActiveRecord
assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id')
end
+ def test_creates_reference_id_with_specified_type
+ add_reference table_name, :user, type: :string
+ assert column_exists?(table_name, :user_id, :string)
+ end
+
def test_deletes_reference_id_column
remove_reference table_name, :supplier
assert_not column_exists?(table_name, :supplier_id, :integer)
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 2a7fafc559..ba39fb1dec 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -42,13 +42,8 @@ module ActiveRecord
def test_rename_table
rename_table :test_models, :octopi
- # Using explicit id in insert for compatibility across all databases
- connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
-
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
-
assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
end
@@ -57,10 +52,7 @@ module ActiveRecord
rename_table :test_models, :octopi
- # Using explicit id in insert for compatibility across all databases
- connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
index = connection.indexes(:octopi).first
@@ -82,7 +74,17 @@ module ActiveRecord
pk, seq = connection.pk_and_sequence_for('octopi')
- assert_equal "octopi_#{pk}_seq", seq
+ assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq
+ end
+
+ def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
+ enable_uuid_ossp!(connection)
+ connection.create_table :cats, id: :uuid
+ assert_nothing_raised { rename_table :cats, :felines }
+ assert connection.table_exists? :felines
+ ensure
+ connection.drop_table :cats if connection.table_exists? :cats
+ connection.drop_table :felines if connection.table_exists? :felines
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 1bda472d23..ef3f073472 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -11,7 +11,13 @@ require MIGRATIONS_ROOT + "/rename/1_we_need_things"
require MIGRATIONS_ROOT + "/rename/2_rename_things"
require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
-class BigNumber < ActiveRecord::Base; end
+class BigNumber < ActiveRecord::Base
+ unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
+ attribute :value_of_e, Type::Integer.new
+ end
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+end
class Reminder < ActiveRecord::Base; end
@@ -33,7 +39,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 = ""
@@ -57,7 +63,7 @@ class MigrationTest < ActiveRecord::TestCase
end
Person.connection.remove_column("people", "first_name") rescue nil
Person.connection.remove_column("people", "middle_name") rescue nil
- Person.connection.add_column("people", "first_name", :string, :limit => 40)
+ Person.connection.add_column("people", "first_name", :string)
Person.reset_column_information
end
@@ -327,47 +333,6 @@ class MigrationTest < ActiveRecord::TestCase
Reminder.reset_table_name
end
- 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_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_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
@@ -585,7 +550,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
@@ -602,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal 8, columns.size
[:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal 0, column(:age).default
+ assert_equal '0', column(:age).default
end
def test_removing_columns
@@ -930,4 +895,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.logger = old
end
+
+ private
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 3f9854200d..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
@@ -93,7 +92,7 @@ module ActiveRecord
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/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 9124105e6d..e87773df94 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -18,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?
@@ -112,6 +112,34 @@ class ModulesTest < ActiveRecord::TestCase
classes.each(&:reset_table_name)
end
+ def test_module_table_name_suffix
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
+ assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ end
+
+ def test_module_table_name_suffix_with_global_suffix
+ classes = [ MyApplication::Business::Company,
+ MyApplication::Business::Firm,
+ MyApplication::Business::Client,
+ MyApplication::Business::Client::Contact,
+ MyApplication::Business::Developer,
+ MyApplication::Business::Project,
+ MyApplication::Business::Suffixed::Company,
+ MyApplication::Business::Suffixed::Nested::Company,
+ MyApplication::Billing::Account ]
+
+ ActiveRecord::Base.table_name_suffix = '_global'
+ classes.each(&:reset_table_name)
+ assert_equal 'companies_global', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
+ assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ ensure
+ ActiveRecord::Base.table_name_suffix = ''
+ classes.each(&:reset_table_name)
+ end
+
def test_compute_type_can_infer_class_name_of_sibling_inside_module
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index c70a8f296f..14d4ef457d 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -240,8 +240,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
Topic.skip_time_zone_conversion_for_attributes = []
end
- # Oracle, and Sybase do not have a TIME datatype.
- unless current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ unless current_adapter?(:OracleAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
attributes = {
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/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index b9f0624f76..2170fe6118 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/aircraft'
require 'models/post'
require 'models/comment'
require 'models/author'
@@ -19,7 +20,7 @@ require 'models/toy'
require 'rexml/document'
class PersistenceTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys
# Oracle UPDATE does not support ORDER BY
unless current_adapter?(:OracleAdapter)
@@ -233,6 +234,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
@@ -240,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_create_columns_not_equal_attributes
- topic = Topic.allocate.init_with(
- 'attributes' => {
- 'title' => 'Another New Topic',
- 'does_not_exist' => 'test'
- }
+ topic = Topic.instantiate(
+ 'title' => 'Another New Topic',
+ 'does_not_exist' => 'test'
)
assert_nothing_raised { topic.save }
end
@@ -290,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topic_reloaded = Topic.allocate
- topic_reloaded.init_with(
- 'attributes' => topic.attributes.merge('does_not_exist' => 'test')
- )
+ topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test'))
topic_reloaded.title = 'A New Topic'
assert_nothing_raised { topic_reloaded.save }
end
@@ -323,6 +328,15 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal "Reply", topic.type
end
+ def test_update_sti_subclass_type
+ assert_instance_of Topic, topics(:first)
+
+ reply = topics(:first).becomes!(Reply)
+ assert_instance_of Reply, reply
+ reply.save!
+ assert_instance_of Reply, Reply.find(reply.id)
+ end
+
def test_update_after_create
klass = Class.new(Topic) do
def self.name; 'Topic'; end
@@ -452,7 +466,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
@@ -495,14 +509,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
topic.reload
- topic.update_column(:content, "You too")
+ topic.update_column(:content, "--- You too\n...\n")
assert_equal [], topic.changed
topic.reload
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
assert_equal [], topic.changed
end
@@ -523,7 +537,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
@@ -586,14 +600,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update({ "content" => "Have a nice day", :author_name => "Jose" })
+ topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" })
topic.reload
- topic.update_columns({ content: "You too", "author_name" => "Sebastian" })
+ topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" })
assert_equal [], topic.changed
topic.reload
- topic.update_columns({ content: "Have a nice day", author_name: "Jose" })
+ topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" })
assert_equal [], topic.changed
end
@@ -620,7 +634,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
@@ -820,4 +834,47 @@ class PersistenceTest < ActiveRecord::TestCase
end
end
+ def test_persist_inherited_class_with_different_table_name
+ minimalistic_aircrafts = Class.new(Minimalistic) do
+ self.table_name = "aircraft"
+ end
+
+ assert_difference "Aircraft.count", 1 do
+ aircraft = minimalistic_aircrafts.create(name: "Wright Flyer")
+ aircraft.name = "Wright Glider"
+ aircraft.save
+ end
+
+ assert_equal "Wright Glider", Aircraft.last.name
+ end
+
+ def test_instantiate_creates_a_new_instance
+ post = Post.instantiate("title" => "appropriate documentation", "type" => "SpecialPost")
+ assert_equal "appropriate documentation", post.title
+ assert_instance_of SpecialPost, post
+
+ # body was not initialized
+ assert_raises ActiveModel::MissingAttributeError do
+ post.body
+ end
+ end
+
+ def test_reload_removes_custom_selects
+ post = Post.select('posts.*, 1 as wibble').last!
+
+ assert_equal 1, post[:wibble]
+ assert_nil post.reload[:wibble]
+ end
+
+ def test_find_via_reload
+ post = Post.new
+
+ assert post.new_record?
+
+ post.id = 1
+ post.reload
+
+ assert_equal "Welcome to the weblog", post.title
+ assert_not post.new_record?
+ end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index 626c6aeaf8..8eea10143f 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -10,7 +10,7 @@ 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 }
@@ -48,4 +48,4 @@ class PooledConnectionsTest < ActiveRecord::TestCase
def add_record(name)
ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
end
-end unless current_adapter?(:FrontBase) || in_memory_db?
+end unless in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 51ddd406ed..f19a6ea5e3 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -5,6 +5,7 @@ require 'models/subscriber'
require 'models/movie'
require 'models/keyboard'
require 'models/mixed_case_monkey'
+require 'models/dashboard'
class PrimaryKeysTest < ActiveRecord::TestCase
fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
@@ -92,6 +93,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_prefix
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
ActiveRecord::Base.primary_key_prefix_type = :table_name
Topic.reset_primary_key
assert_equal "topicid", Topic.primary_key
@@ -103,6 +105,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase
ActiveRecord::Base.primary_key_prefix_type = nil
Topic.reset_primary_key
assert_equal "id", Topic.primary_key
+ ensure
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
def test_delete_should_quote_pkey
@@ -131,14 +135,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers')
+ assert_equal 'id', klass.primary_key
end
end
def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_nil ActiveRecord::Base.connection.primary_key('developers_projects')
+ assert_nil klass.primary_key
end
end
@@ -149,42 +161,19 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key
end
- def test_two_models_with_same_table_but_different_primary_key
- k1 = Class.new(ActiveRecord::Base)
- k1.table_name = 'posts'
- k1.primary_key = 'id'
-
- k2 = Class.new(ActiveRecord::Base)
- k2.table_name = 'posts'
- k2.primary_key = 'title'
-
- assert k1.columns.find { |c| c.name == 'id' }.primary
- assert !k1.columns.find { |c| c.name == 'title' }.primary
- assert k1.columns_hash['id'].primary
- assert !k1.columns_hash['title'].primary
-
- assert !k2.columns.find { |c| c.name == 'id' }.primary
- assert k2.columns.find { |c| c.name == 'title' }.primary
- assert !k2.columns_hash['id'].primary
- assert k2.columns_hash['title'].primary
- end
-
- def test_models_with_same_table_have_different_columns
- k1 = Class.new(ActiveRecord::Base)
- k1.table_name = 'posts'
-
- k2 = Class.new(ActiveRecord::Base)
- k2.table_name = 'posts'
-
- k1.columns.zip(k2.columns).each do |col1, col2|
- 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
+
+ def test_primary_key_update_with_custom_key_name
+ dashboard = Dashboard.create!(dashboard_id: '1')
+ dashboard.id = '2'
+ dashboard.save!
+
+ dashboard = Dashboard.first
+ assert_equal '2', dashboard.id
+ end
end
class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
@@ -219,3 +208,29 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
end
end
end
+
+if current_adapter?(:PostgreSQLAdapter)
+ class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Widget < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:widgets, id: :bigserial) { |t| }
+ end
+
+ teardown do
+ @connection.drop_table :widgets
+ end
+
+ def test_bigserial_primary_key
+ assert_equal "id", Widget.primary_key
+ assert_equal :integer, Widget.columns_hash[Widget.primary_key].type
+
+ widget = Widget.create!
+ assert_not_nil widget.id
+ end
+ end
+end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index da8ae672fe..9d89d6a1e8 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -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
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index e2439b9a24..1d6ae2f67f 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -3,14 +3,6 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class QuotingTest < ActiveRecord::TestCase
- class FakeColumn < ActiveRecord::ConnectionAdapters::Column
- attr_accessor :type
-
- def initialize type
- @type = type
- end
- end
-
def setup
@quoter = Class.new { include Quoting }.new
end
@@ -91,69 +83,56 @@ module ActiveRecord
def test_quote_with_quoted_id
assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil)
- assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo')
end
def test_quote_nil
assert_equal 'NULL', @quoter.quote(nil, nil)
- assert_equal 'NULL', @quoter.quote(nil, 'foo')
end
def test_quote_true
assert_equal @quoter.quoted_true, @quoter.quote(true, nil)
- assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer))
end
def test_quote_false
assert_equal @quoter.quoted_false, @quoter.quote(false, nil)
- assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer))
end
def test_quote_float
float = 1.2
assert_equal float.to_s, @quoter.quote(float, nil)
- assert_equal float.to_s, @quoter.quote(float, Object.new)
end
def test_quote_fixnum
fixnum = 1
assert_equal fixnum.to_s, @quoter.quote(fixnum, nil)
- assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new)
end
def test_quote_bignum
bignum = 1 << 100
assert_equal bignum.to_s, @quoter.quote(bignum, nil)
- assert_equal bignum.to_s, @quoter.quote(bignum, Object.new)
end
def test_quote_bigdecimal
bigdec = BigDecimal.new((1 << 100).to_s)
assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil)
- assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new)
end
def test_dates_and_times
@quoter.extend(Module.new { def quoted_date(value) 'lol' end })
assert_equal "'lol'", @quoter.quote(Date.today, nil)
- assert_equal "'lol'", @quoter.quote(Date.today, Object.new)
assert_equal "'lol'", @quoter.quote(Time.now, nil)
- assert_equal "'lol'", @quoter.quote(Time.now, Object.new)
assert_equal "'lol'", @quoter.quote(DateTime.now, nil)
- assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new)
end
def test_crazy_object
crazy = Class.new.new
expected = "'#{YAML.dump(crazy)}'"
assert_equal expected, @quoter.quote(crazy, nil)
- assert_equal expected, @quoter.quote(crazy, Object.new)
end
def test_crazy_object_calls_quote_string
crazy = Class.new { def initialize; @lol = 'lo\l' end }.new
assert_match "lo\\\\l", @quoter.quote(crazy, nil)
- assert_match "lo\\\\l", @quoter.quote(crazy, Object.new)
end
def test_quote_string_no_column
@@ -165,36 +144,13 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote(string, nil)
end
- def test_quote_string_int_column
- assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer))
- assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer))
- end
-
- def test_quote_string_float_column
- assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float))
- assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float))
- end
-
- def test_quote_as_mb_chars_binary_column
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary))
- end
-
- def test_quote_binary_without_string_to_binary
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary))
- end
-
def test_string_with_crazy_column
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo))
+ assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
end
def test_quote_duration
assert_equal "1800", @quoter.quote(30.minutes)
end
-
- def test_quote_duration_int_column
- assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer))
- end
end
end
end
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index b62a41c08e..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_threadsafe?; 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 d7ad5ed29f..84abaf0291 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -63,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
@@ -80,24 +80,38 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal :integer, @first.column_for_attribute("id").type
end
+ def test_non_existent_columns_return_nil
+ assert_deprecated do
+ assert_nil @first.column_for_attribute("attribute_that_doesnt_exist")
+ end
+ end
+
def test_reflection_klass_for_nested_class_name
- reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
assert_nothing_raised do
assert_equal MyApplication::Business::Company, reflection.klass
end
end
+ def test_irregular_reflection_class_name
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'plural_irregular', 'plurales_irregulares'
+ end
+ reflection = ActiveRecord::Reflection.create(: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
+ :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
)
reflection_for_balance = AggregateReflection.new(
- :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
)
reflection_for_gps_location = AggregateReflection.new(
- :composed_of, :gps_location, nil, { }, Customer
+ :gps_location, nil, { }, Customer
)
assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
@@ -121,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_many_reflection
- reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
+ reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
@@ -133,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_one_reflection
- reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
+ reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
assert_equal Account, Firm.reflect_on_association(:account).klass
@@ -192,7 +206,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_reflection_should_not_raise_error_when_compared_to_other_object
- assert_nothing_raised { Firm.reflections[:clients] == Object.new }
+ assert_not_equal Object.new, Firm._reflections['clients']
+ end
+
+ def test_has_and_belongs_to_many_reflection
+ assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro
+ assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name
end
def test_has_many_through_reflection
@@ -265,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_association_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
+ reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
through = Class.new(ActiveRecord::Reflection::ThroughReflection) {
define_method(:source_reflection) { reflection }
- }.new(:fuu, :edge, nil, {}, Author)
+ }.new(reflection)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
@@ -280,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_active_record_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge)
+ reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
@@ -298,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_default_association_validation
- assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
end
def test_always_validate_association_if_explicit
- assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate?
end
def test_validate_association_if_autosave
- assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
end
def test_never_validate_association_if_explicit
- assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
end
def test_foreign_key
@@ -345,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'categories_products', reflection.join_table
end
@@ -358,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'catalog_categories_products', reflection.join_table
end
@@ -371,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_content_pages', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
reflection.stubs(:klass).returns(page)
assert_equal 'catalog_categories_content_pages', reflection.join_table
end
@@ -384,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product)
reflection.stubs(:klass).returns(category)
assert_equal 'product_categories', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
reflection.stubs(:klass).returns(product)
assert_equal 'product_categories', reflection.join_table
end
+ def test_includes_accepts_symbols
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs
+ end
+ end
+
+ def test_includes_accepts_strings
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs
+ end
+ end
+
+ def test_reflect_on_association_accepts_symbols
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association(:departments).name, :departments
+ end
+ end
+
+ def test_reflect_on_association_accepts_strings
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association("departments").name, :departments
+ end
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 9b2bfed039..29c9d0e2af 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -32,7 +32,7 @@ module ActiveRecord
: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
+ :to_ary, :to_set, :to_xml, :to_yaml, :join
]
ARRAY_DELEGATES.each do |method|
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index 23500bf5d8..2b5c2fd5a4 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -17,10 +17,9 @@ class RelationMergingTest < ActiveRecord::TestCase
end
def test_relation_to_sql
- sql = Post.connection.unprepared_statement do
- Post.first.comments.to_sql
- end
- assert_no_match(/\?/, sql)
+ post = Post.first
+ sql = post.comments.to_sql
+ assert_match(/.?post_id.? = #{post.id}\Z/i, sql)
end
def test_relation_merging_with_arel_equalities_keeps_last_equality
@@ -81,31 +80,20 @@ class RelationMergingTest < ActiveRecord::TestCase
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
+ expected = [left.bind_values[1]] + right.bind_values
merged = left.merge(right)
- assert_equal expected, merged.where_values
+ 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_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)
-
- merged = left.merge(right)
- assert_equal [], merged.bind_values
- end
-
def test_merging_keeps_lhs_bind_parameters
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)
@@ -113,21 +101,18 @@ class RelationMergingTest < ActiveRecord::TestCase
end
def test_merging_reorders_bind_params
- post = Post.first
- id_column = Post.columns_hash['id']
- title_column = Post.columns_hash['title']
-
- bv = Post.connection.substitute_at id_column, 0
-
- right = Post.where(id: bv)
- right.bind_values += [[id_column, post.id]]
-
- left = Post.where(title: bv)
- left.bind_values += [[title_column, post.title]]
+ post = Post.first
+ right = Post.where(id: 1)
+ left = Post.where(title: post.title)
merged = left.merge(right)
assert_equal post, merged.first
end
+
+ def test_merging_compares_symbols_and_strings_as_equal
+ post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.")
+ assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body
+ end
end
class MergingDifferentRelationsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index 4fafa668fb..1da5c36e1c 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -24,13 +24,18 @@ module ActiveRecord
@relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table
end
- (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope]).each do |method|
+ (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
@@ -107,10 +112,18 @@ module ActiveRecord
end
test 'reverse_order!' do
- assert relation.reverse_order!.equal?(relation)
- assert relation.reverse_order_value
+ 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 !relation.reverse_order_value
+
+ assert_equal 'title ASC', relation.order_values.first
+ assert_equal 'comments_count DESC', relation.order_values.last
end
test 'create_with!' do
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
index 14a8d97d36..4057835688 100644
--- a/activerecord/test/cases/relation/predicate_builder_test.rb
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -5,10 +5,10 @@ module ActiveRecord
class PredicateBuilderTest < ActiveRecord::TestCase
def test_registering_new_handlers
PredicateBuilder.register_handler(Regexp, proc do |column, value|
- Arel::Nodes::InfixOperation.new('~', column, value.source)
+ Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source))
end)
- assert_match %r{["`]topics["`].["`]title["`] ~ 'rails'}i, Topic.where(title: /rails/).to_sql
+ 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 fd2420cb88..b9e69bdb08 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -12,13 +12,19 @@ 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
@@ -30,13 +36,13 @@ module ActiveRecord
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
@@ -44,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
@@ -76,41 +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')
- expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'alone')
assert_equal 1, relation.where_values.size
- 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 'alone', bind.last
end
def test_rewhere_with_multiple_overwriting_conditions
relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
- title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone')
- body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'again')
-
assert_equal 2, relation.where_values.size
- assert_equal title_expected, relation.where_values.first
- assert_equal body_expected, relation.where_values.second
+
+ 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')
- title_expected = Arel::Nodes::Equality.new(Post.arel_table['title'], 'alone')
- body_expected = Arel::Nodes::Equality.new(Post.arel_table['body'], 'world')
-
assert_equal 2, relation.where_values.size
- assert_equal body_expected, relation.where_values.first
- assert_equal title_expected, relation.where_values.second
+
+ 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 937f226b1d..a6a36a6fd9 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -6,10 +6,11 @@ require 'models/post'
require 'models/comment'
require 'models/edge'
require 'models/topic'
+require 'models/binary'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors
+ fixtures :posts, :edges, :authors, :binaries
def test_where_copies_bind_params
author = authors(:david)
@@ -179,5 +180,35 @@ module ActiveRecord
assert_equal 4, Edge.where(blank).order("sink_id").to_a.size
end
end
+
+ def test_where_with_integer_for_string_column
+ count = Post.where(:title => 0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_float_for_string_column
+ count = Post.where(:title => 0.0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_boolean_for_string_column
+ count = Post.where(:title => false).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_decimal_for_string_column
+ count = Post.where(:title => BigDecimal.new(0)).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_duration_for_string_column
+ count = Post.where(:title => 0.seconds).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_integer_for_binary_column
+ count = Binary.where(:data => 0).count
+ assert_equal 0, count
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 15611656fd..3280945d09 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -16,6 +16,10 @@ module ActiveRecord
def self.connection
Post.connection
end
+
+ def self.table_name
+ 'fake_table'
+ end
end
def test_construction
@@ -231,5 +235,33 @@ 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
+
+ class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
+ def type
+ :string
+ end
+
+ def type_cast_from_database(value)
+ raise value unless value == "type cast for database"
+ "type cast from database"
+ end
+
+ def type_cast_for_database(value)
+ raise value unless value == "value from user"
+ "type cast for database"
+ end
+ end
+
+ class UpdateAllTestModel < ActiveRecord::Base
+ self.table_name = 'posts'
+
+ attribute :body, EnsureRoundTripTypeCasting.new
+ end
+
+ def test_update_all_goes_through_normal_type_casting
+ UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI
+
+ assert_equal "type cast from database", UpdateAllTestModel.first.body
+ end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index e390d37871..88df997a2f 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
@@ -151,11 +152,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a
end
- def test_finding_with_subquery_without_select
- relation = Topic.where(:approved => true)
- assert_equal relation.to_a, Topic.from(relation).to_a
+ 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)
@@ -168,7 +172,6 @@ class RelationTest < ActiveRecord::TestCase
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 5, topics.to_a.size
@@ -191,8 +194,33 @@ class RelationTest < ActiveRecord::TestCase
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
@@ -338,6 +366,60 @@ class RelationTest < ActiveRecord::TestCase
assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
end
+ def test_null_relation_sum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).sum(:id)
+ assert_equal 0, ac.engines.count
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).sum(:id)
+ assert_equal 0, ac.engines.count
+ 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_null_relation_size
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).size
+ assert_equal 0, ac.engines.size
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).size
+ assert_equal 0, ac.engines.size
+ end
+
+ def test_null_relation_average
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
+ assert_equal nil, ac.engines.average(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
+ assert_equal nil, ac.engines.average(:id)
+ end
+
+ def test_null_relation_minimum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
+ assert_equal nil, ac.engines.minimum(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
+ assert_equal nil, ac.engines.minimum(:id)
+ end
+
+ def test_null_relation_maximum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
+ assert_equal nil, ac.engines.maximum(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
+ assert_equal nil, ac.engines.maximum(:id)
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -546,6 +628,12 @@ class RelationTest < ActiveRecord::TestCase
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 }
@@ -610,7 +698,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
@@ -655,6 +743,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], relation.to_a
end
+ def test_typecasting_where_with_array
+ ids = Author.pluck(:id)
+ slugs = ids.map { |id| "#{id}-as-a-slug" }
+
+ assert_equal Author.all.to_a, Author.where(id: slugs).to_a
+ end
+
def test_find_all_using_where_with_relation
david = authors(:david)
# switching the lines below would succeed in current rails
@@ -742,7 +837,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?
@@ -787,8 +882,22 @@ class RelationTest < ActiveRecord::TestCase
assert davids.loaded?
end
- def test_delete_all_limit_error
+ def test_delete_all_with_unpermitted_relation_raises_error
assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(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
@@ -821,6 +930,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
@@ -831,6 +951,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
@@ -1339,6 +1467,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
@@ -1383,6 +1519,18 @@ class RelationTest < ActiveRecord::TestCase
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
@@ -1558,10 +1706,8 @@ class RelationTest < ActiveRecord::TestCase
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
@@ -1571,8 +1717,7 @@ 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)
@@ -1580,19 +1725,15 @@ class RelationTest < ActiveRecord::TestCase
end
def test_merging_reorders_bind_params
- post = Post.first
- id_column = Post.columns_hash['id']
- title_column = Post.columns_hash['title']
-
- bv = Post.connection.substitute_at id_column, 0
-
- right = Post.where(id: bv)
- right.bind_values += [[id_column, post.id]]
-
- left = Post.where(title: bv)
- left.bind_values += [[title_column, post.title]]
+ 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
+
+ def test_relation_join_method
+ assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",")
+ end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index 2131b32a0c..d6decafad9 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -10,7 +10,7 @@ module ActiveRecord
])
end
- def test_to_hash_returns_row_hashes
+ test "to_hash returns row_hashes" do
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'},
@@ -18,13 +18,13 @@ module ActiveRecord
], result.to_hash
end
- def test_each_with_block_returns_row_hashes
+ test "each with block returns row hashes" do
result.each do |row|
assert_equal ['col_1', 'col_2'], row.keys
end
end
- def test_each_without_block_returns_an_enumerator
+ test "each without block returns an enumerator" do
result.each.with_index do |row, index|
assert_equal ['col_1', 'col_2'], row.keys
assert_kind_of Integer, index
@@ -32,9 +32,45 @@ module ActiveRecord
end
if Enumerator.method_defined? :size
- def test_each_without_block_returns_a_sized_enumerator
+ test "each without block returns a sized enumerator" do
assert_equal 3, result.each.size
end
end
+
+ test "cast_values returns rows after type casting" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, 2.2], [3, 4.4]], result.cast_values
+ end
+
+ test "cast_values uses identity type for unknown types" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values
+ end
+
+ test "cast_values returns single dimensional array if single column" do
+ values = [["1.1"], ["3.3"]]
+ columns = ["col1"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [1, 3], result.cast_values
+ end
+
+ test "cast_values can receive types to use instead" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new)
+ end
end
end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 954eab8022..dca85fb5eb 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -51,4 +51,31 @@ class SanitizeTest < ActiveRecord::TestCase
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 c085663efb..4e71d04bc0 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,10 +1,9 @@
require "cases/helper"
+require 'support/schema_dumping_helper'
class SchemaDumperTest < ActiveRecord::TestCase
- def setup
- super
+ setup do
ActiveRecord::SchemaMigration.create_table
- @stream = StringIO.new
end
def standard_dump
@@ -25,7 +24,8 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_magic_comment
- assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump
+ output = standard_dump
+ assert_match "# encoding: #{@stream.external_encoding.name}", output
end
def test_schema_dump
@@ -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|point)\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,7 +188,7 @@ 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
@@ -319,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
@@ -346,9 +353,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
# Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers
if current_adapter?(:OracleAdapter)
- assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output
+ assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output
else
- assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output
+ assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output
end
end
@@ -365,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{create_table "subscribers", id: false}, output
end
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues
+ output = standard_dump
+ assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output)
+ end
+ end
+
class CreateDogMigration < ActiveRecord::Migration
def up
+ create_table("dog_owners") do |t|
+ end
+
create_table("dogs") do |t|
t.column :name, :string
+ t.column :owner_id, :integer
end
add_index "dogs", [:name]
+ add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
+ drop_table("dog_owners")
end
end
@@ -387,13 +407,45 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
assert_no_match %r{create_table "foo_.+_bar"}, output
- assert_no_match %r{create_index "foo_.+_bar"}, output
+ assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ assert_no_match %r{add_foreign_key "foo_.+_bar"}, output
+ assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output
+ end
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
$stdout = original
end
+end
+
+class SchemaDumperDefaultsTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :defaults, force: true do |t|
+ t.string :string_with_default, default: "Hello!"
+ t.date :date_with_default, default: '2014-06-05'
+ t.datetime :datetime_with_default, default: "2014-06-05 07:17:04"
+ t.time :time_with_default, default: "07:17:04"
+ end
+ end
+
+ teardown do
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS defaults'
+ end
+
+ def test_schema_dump_defaults_with_universally_supported_types
+ output = dump_table_schema('defaults')
+ assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output
+ assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output
+ assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output
+ assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output
+ end
end
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 170e9a49eb..9a4d8c6740 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -395,4 +395,22 @@ class DefaultScopingTest < ActiveRecord::TestCase
threads.each(&:join)
end
end
+
+ 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
+
+ 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 f0ad9ebb8a..59ec2dd6a4 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -291,6 +291,9 @@ class NamedScopingTest < ActiveRecord::TestCase
:relation, # private class method on AR::Base
:new, # redefined class method on AR::Base
:all, # a default scope
+ :public,
+ :protected,
+ :private
]
non_conflicts = [
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index 0018fc06f2..8e512e118a 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -11,6 +11,10 @@ require 'models/reference'
class RelationScopingTest < ActiveRecord::TestCase
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
+ setup do
+ developers(:david)
+ end
+
def test_reverse_order
assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order
end
@@ -192,8 +196,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
@@ -259,7 +264,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
end
end
-class HasManyScopingTest< ActiveRecord::TestCase
+class HasManyScopingTest < ActiveRecord::TestCase
fixtures :comments, :posts, :people, :references
def setup
@@ -305,7 +310,7 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
end
-class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
+class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
fixtures :posts, :categories, :categories_posts
def setup
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index c46060a646..3f52e80e11 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -1,8 +1,11 @@
require "cases/helper"
require 'models/contact'
require 'models/topic'
+require 'models/book'
class SerializationTest < ActiveRecord::TestCase
+ fixtures :books
+
FORMATS = [ :xml, :json ]
def setup
@@ -65,4 +68,28 @@ class SerializationTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
+
+ def test_read_attribute_for_serialization_with_format_without_method_missing
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new
+ assert_nil book.read_attribute_for_serialization(:format)
+ end
+
+ def test_read_attribute_for_serialization_with_format_after_init
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new(format: 'paperback')
+ assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ end
+
+ def test_read_attribute_for_serialization_with_format_after_find
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.find(books(:awdr).id)
+ assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index bc67da8d27..f8d87a3661 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -3,20 +3,29 @@ require 'models/topic'
require 'models/reply'
require 'models/person'
require 'models/traffic_light'
+require 'models/post'
require 'bcrypt'
class SerializedAttributeTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :posts
MyObject = Struct.new :attribute1, :attribute2
- def teardown
- super
+ teardown do
Topic.serialize("content")
end
+ def test_serialize_does_not_eagerly_load_columns
+ assert_no_queries do
+ Topic.reset_column_information
+ Topic.serialize(:content)
+ end
+ end
+
def test_list_of_serialized_attributes
- assert_equal %w(content), Topic.serialized_attributes.keys
+ assert_deprecated do
+ assert_equal %w(content), Topic.serialized_attributes.keys
+ end
end
def test_serialized_attribute
@@ -30,12 +39,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
- def test_serialized_attribute_init_with
- topic = Topic.allocate
- topic.init_with('attributes' => { 'content' => '--- foo' })
- assert_equal 'foo', topic.content
- end
-
def test_serialized_attribute_in_base_class
Topic.serialize("content", Hash)
@@ -47,34 +50,56 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(hash, important_topic.content)
end
- # This test was added to fix GH #4004. Obviously the value returned
- # is not really the value 'before type cast' so we should maybe think
- # about changing that in the future.
- def test_serialized_attribute_before_type_cast_returns_unserialized_value
+ def test_serialized_attributes_from_database_on_subclass
Topic.serialize :content, Hash
- t = Topic.new(content: { foo: :bar })
- assert_equal({ foo: :bar }, t.content_before_type_cast)
+ t = Reply.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.content)
t.save!
- t.reload
- assert_equal({ foo: :bar }, t.content_before_type_cast)
+ t = Reply.last
+ assert_equal({ foo: :bar }, t.content)
end
- def test_serialized_attributes_before_type_cast_returns_unserialized_value
- Topic.serialize :content, Hash
+ def test_serialized_attribute_calling_dup_method
+ Topic.serialize :content, JSON
- t = Topic.new(content: { foo: :bar })
- assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+ orig = Topic.new(content: { foo: :bar })
+ clone = orig.dup
+ assert_equal(orig.content, clone.content)
+ end
+
+ def test_serialized_json_attribute_returns_unserialized_value
+ Topic.serialize :content, JSON
+ my_post = posts(:welcome)
+
+ t = Topic.new(content: my_post)
t.save!
t.reload
- assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+
+ assert_instance_of(Hash, t.content)
+ assert_equal(my_post.id, t.content["id"])
+ assert_equal(my_post.title, t.content["title"])
end
- def test_serialized_attribute_calling_dup_method
+ def test_json_read_legacy_null
Topic.serialize :content, JSON
- t = Topic.new(:content => { :foo => :bar }).dup
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ # Force a row to have a JSON "null" instead of a database NULL (this is how
+ # null values are saved on 4.1 and before)
+ id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')"
+ t = Topic.find(id)
+
+ assert_nil t.content
+ end
+
+ def test_json_read_db_null
+ Topic.serialize :content, JSON
+
+ # Force a row to have a database NULL instead of a JSON "null"
+ id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)"
+ t = Topic.find(id)
+
+ assert_nil t.content
end
def test_serialized_attribute_declared_in_subclass
@@ -117,8 +142,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type
Topic.serialize(:content, Hash)
- topic = Topic.new(:content => "string")
- assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save }
+ assert_raise(ActiveRecord::SerializationTypeMismatch) do
+ topic = Topic.new(content: 'string')
+ topic.save
+ end
end
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
@@ -169,45 +196,22 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_with_coder
- coder = Class.new {
- # Identity
- def load(thing)
- thing
- end
-
- # base 64
- def dump(thing)
- [thing].pack('m')
+ some_class = Struct.new(:foo) do
+ def self.dump(value)
+ value.foo
end
- }.new
- Topic.serialize(:content, coder)
- s = 'hello world'
- topic = Topic.new(:content => s)
- assert topic.save
- topic = topic.reload
- assert_equal [s].pack('m'), topic.content
- end
-
- def test_serialize_with_bcrypt_coder
- crypt_coder = Class.new {
- def load(thing)
- return unless thing
- BCrypt::Password.new thing
+ def self.load(value)
+ new(value)
end
+ end
- def dump(thing)
- BCrypt::Password.create(thing).to_s
- end
- }.new
-
- Topic.serialize(:content, crypt_coder)
- password = 'password'
- topic = Topic.new(:content => password)
- assert topic.save
- topic = topic.reload
- assert_kind_of BCrypt::Password, topic.content
- assert_equal(true, topic.content == password, 'password should equal')
+ Topic.serialize(:content, some_class)
+ topic = Topic.new(:content => some_class.new('my value'))
+ topic.save!
+ topic.reload
+ assert_kind_of some_class, topic.content
+ assert_equal topic.content, some_class.new('my value')
end
def test_serialize_attribute_via_select_method_when_time_zone_available
@@ -236,13 +240,19 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal [], light.long_state
end
- def test_serialized_column_should_not_be_wrapped_twice
- Topic.serialize(:content, MyObject)
+ def test_serialized_column_should_unserialize_after_update_column
+ t = Topic.create(content: "first")
+ assert_equal("first", t.content)
- myobj = MyObject.new('value1', 'value2')
- Topic.create(content: myobj)
- Topic.create(content: myobj)
- type = Topic.column_types["content"]
- assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type)
+ t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second"))
+ assert_equal("second", t.content)
+ end
+
+ def test_serialized_column_should_unserialize_after_update_attribute
+ t = Topic.create(content: "first")
+ assert_equal("first", t.content)
+
+ t.update_attribute(:content, "second")
+ assert_equal("second", t.content)
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 978cee9cfb..e9cdf94c99 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -163,22 +163,24 @@ class StoreTest < ActiveRecord::TestCase
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 "stored_attributes are tracked per subclass" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(first_model) do
+ store_accessor :data, :width, :height
+ end
+ third_model = Class.new(first_model) do
+ store_accessor :data, :area, :volume
+ 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 [:color], first_model.stored_attributes[:data]
+ assert_equal [:color, :width, :height], second_model.stored_attributes[:data]
+ assert_equal [:color, :area, :volume], third_model.stored_attributes[:data]
+ end
- assert_equal attributes, @john.attributes_for_coder
+ test "YAML coder initializes the store when a Nil value is given" do
+ assert_equal({}, @john.params)
end
test "dump, load and dump again a model" do
@@ -187,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase
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 bf9e14fa4f..01d373b691 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -205,7 +205,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_ip
+ def test_drops_configurations_with_local_ip
@configurations[:development].merge!('host' => '127.0.0.1')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -213,7 +213,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_host
+ def test_drops_configurations_with_local_host
@configurations[:development].merge!('host' => 'localhost')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -221,7 +221,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_blank_hosts
+ def test_drops_configurations_with_blank_hosts
@configurations[:development].merge!('host' => nil)
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -241,7 +241,7 @@ module ActiveRecord
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
- def test_creates_current_environment_database
+ def test_drops_current_environment_database
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'prod-db')
@@ -273,6 +273,19 @@ module ActiveRecord
end
end
+ class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ def test_migrate_receives_correct_env_vars
+ verbose, version = ENV['VERBOSE'], ENV['VERSION']
+
+ ENV['VERBOSE'] = 'false'
+ ENV['VERSION'] = '4'
+
+ ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ ensure
+ ENV['VERBOSE'], ENV['VERSION'] = verbose, version
+ end
+ end
class DatabaseTasksPurgeTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -285,6 +298,34 @@ module ActiveRecord
end
end
+ class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
+ def test_purges_current_environment_database
+ configurations = {
+ 'development' => {'database' => 'dev-db'},
+ 'test' => {'database' => 'test-db'},
+ 'production' => {'database' => 'prod-db'}
+ }
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'prod-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_current('production')
+ end
+ end
+
+ class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
+ def test_purge_all_local_configurations
+ configurations = {:development => {'database' => 'my-db'}}
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'my-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 3e3a2828f3..f58535f044 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -1,5 +1,6 @@
require 'cases/helper'
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
module ActiveRecord
class MysqlDBCreateTest < ActiveRecord::TestCase
def setup
@@ -196,8 +197,8 @@ module ActiveRecord
ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
- def test_establishes_connection_to_test_database
- ActiveRecord::Base.expects(:establish_connection).with(:test)
+ def test_establishes_connection_to_the_appropriate_database
+ ActiveRecord::Base.expects(:establish_connection).with(@configuration)
ActiveRecord::Tasks::DatabaseTasks.purge @configuration
end
@@ -307,3 +308,4 @@ module ActiveRecord
end
end
+end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 6ea225178f..0d574d071c 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -1,5 +1,6 @@
require 'cases/helper'
+if current_adapter?(:PostgreSQLAdapter)
module ActiveRecord
class PostgreSQLDBCreateTest < ActiveRecord::TestCase
def setup
@@ -241,3 +242,4 @@ module ActiveRecord
end
end
+end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index da3471adf9..750d5e42dc 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'pathname'
+if current_adapter?(:SQLite3Adapter)
module ActiveRecord
class SqliteDBCreateTest < ActiveRecord::TestCase
def setup
@@ -189,3 +190,4 @@ module ActiveRecord
end
end
end
+end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 4476ce3410..23a170388e 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -10,19 +10,34 @@ module ActiveRecord
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
+ assert_equal expected.to_s, actual.to_s, message
end
- def assert_sql(*patterns_to_match)
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+
+ def capture_sql
SQLCounter.clear_log
yield
- SQLCounter.log_all
+ SQLCounter.log_all.dup
+ end
+
+ def assert_sql(*patterns_to_match)
+ capture_sql { yield }
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 8195e8a08a..abf6becc17 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'support/ddl_helper'
require 'models/developer'
require 'models/owner'
require 'models/pet'
@@ -9,26 +10,11 @@ require 'models/task'
class TimestampTest < ActiveRecord::TestCase
fixtures :developers, :owners, :pets, :toys, :cars, :tasks
- # Model with timestamp attribute accessors, but without timestamps columns.
- class TimestampAttributePost < ActiveRecord::Base
- attr_accessor :created_at, :updated_at
- end
-
-
def setup
@developer = Developer.first
@owner = Owner.first
@developer.update_columns(updated_at: Time.now.prev_month)
@previously_updated_at = @developer.updated_at
-
- @connection = ActiveRecord::Base.connection
- @connection.create_table 'timestamp_attribute_posts', :force => true do |t|
- # Must have no timestamps
- end
- end
-
- def teardown
- @connection.execute 'drop table if exists timestamp_attribute_posts'
end
def test_saving_a_changed_record_updates_its_timestamp
@@ -104,6 +90,18 @@ 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
@@ -155,6 +153,25 @@ class TimestampTest < ActiveRecord::TestCase
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
@@ -365,6 +382,19 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal time, pet.updated_at
end
+ def test_timestamp_column_values_are_present_in_the_callbacks
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'people'
+
+ before_create do
+ self.born_at = self.created_at
+ end
+ end
+
+ person = klass.create first_name: 'David'
+ assert_not_equal person.born_at, nil
+ end
+
def test_timestamp_attributes_for_create
toy = Toy.first
assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create)
@@ -394,11 +424,22 @@ class TimestampTest < ActiveRecord::TestCase
toy = Toy.first
assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
end
+end
+
+class TimestampsWithoutTransactionTest < ActiveRecord::TestCase
+ include DdlHelper
+ self.use_transactional_fixtures = false
+
+ class TimestampAttributePost < ActiveRecord::Base
+ attr_accessor :created_at, :updated_at
+ end
def test_do_not_write_timestamps_on_save_if_they_are_not_attributes
- post = TimestampAttributePost.new
- assert_nothing_raised ActiveModel::MissingAttributeError do
- post.run_callbacks :save
+ with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do
+ post = TimestampAttributePost.new(id: 1)
+ post.save! # should not try to assign and persist created_at, updated_at
+ assert_nil post.created_at
+ assert_nil post.updated_at
end
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 1664f1a096..b4849222b8 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,19 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
+ def test_rolling_back_in_a_callback_rollbacks_before_save
+ def @first.before_save_for_transaction
+ raise ActiveRecord::Rollback
+ end
+ assert !@first.approved
+
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
+ assert !Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ end
+
def test_raising_exception_in_nested_transaction_restore_state_in_save
topic = Topic.new
@@ -405,6 +424,26 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_savepoints_name
+ Topic.transaction do
+ assert_nil Topic.connection.current_savepoint_name
+ assert_nil Topic.connection.current_transaction.savepoint_name
+
+ Topic.transaction(requires_new: true) do
+ assert_equal "active_record_1", Topic.connection.current_savepoint_name
+ assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
+
+ Topic.transaction(requires_new: true) do
+ assert_equal "active_record_2", Topic.connection.current_savepoint_name
+ assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name
+ end
+
+ assert_equal "active_record_1", Topic.connection.current_savepoint_name
+ assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
+ end
+ end
+ end
+
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
@@ -507,13 +546,13 @@ class TransactionTest < ActiveRecord::TestCase
def test_transactions_state_from_rollback
connection = Topic.connection
- transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
assert transaction.open?
assert !transaction.state.rolledback?
assert !transaction.state.committed?
- transaction.perform_rollback
+ transaction.rollback
assert transaction.state.rolledback?
assert !transaction.state.committed?
@@ -521,13 +560,13 @@ class TransactionTest < ActiveRecord::TestCase
def test_transactions_state_from_commit
connection = Topic.connection
- transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
assert transaction.open?
assert !transaction.state.rolledback?
assert !transaction.state.committed?
- transaction.perform_commit
+ transaction.commit
assert !transaction.state.rolledback?
assert transaction.state.committed?
diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb
new file mode 100644
index 0000000000..da30de373e
--- /dev/null
+++ b/activerecord/test/cases/type/decimal_test.rb
@@ -0,0 +1,38 @@
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class DecimalTest < ActiveRecord::TestCase
+ def test_type_cast_decimal
+ type = Decimal.new
+ assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0"))
+ assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0)
+ assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1")
+ end
+
+ def test_type_cast_decimal_from_float_with_large_precision
+ type = Decimal.new(precision: ::Float::DIG + 2)
+ assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0)
+ end
+
+ def test_type_cast_decimal_from_rational_with_precision
+ type = Decimal.new(precision: 2)
+ assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
+ type = Decimal.new
+ assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_object_responding_to_d
+ value = Object.new
+ def value.to_d
+ BigDecimal.new("1")
+ end
+ type = Decimal.new
+ assert_equal BigDecimal("1"), type.type_cast_from_user(value)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
new file mode 100644
index 0000000000..420177ed49
--- /dev/null
+++ b/activerecord/test/cases/type/string_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class StringTypeTest < ActiveRecord::TestCase
+ test "type casting" do
+ type = Type::String.new
+ assert_equal "1", type.type_cast_from_user(true)
+ assert_equal "0", type.type_cast_from_user(false)
+ assert_equal "123", type.type_cast_from_user(123)
+ end
+
+ test "values are duped coming out" do
+ s = "foo"
+ type = Type::String.new
+ assert_not_same s, type.type_cast_from_user(s)
+ assert_not_same s, type.type_cast_from_database(s)
+ end
+
+ test "string mutations are detected" do
+ klass = Class.new(Base)
+ klass.table_name = 'authors'
+
+ author = klass.create!(name: 'Sean')
+ assert_not author.changed?
+
+ author.name << ' Griffin'
+ assert author.name_changed?
+
+ author.save!
+ author.reload
+
+ assert_equal 'Sean Griffin', author.name
+ assert_not author.changed?
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb
new file mode 100644
index 0000000000..4e32f92dd0
--- /dev/null
+++ b/activerecord/test/cases/type/type_map_test.rb
@@ -0,0 +1,130 @@
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class TypeMapTest < ActiveRecord::TestCase
+ def test_default_type
+ mapping = TypeMap.new
+
+ assert_kind_of Value, mapping.lookup(:undefined)
+ end
+
+ def test_registering_types
+ boolean = Boolean.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/boolean/i, boolean)
+
+ assert_equal mapping.lookup('boolean'), boolean
+ end
+
+ def test_overriding_registered_types
+ time = Time.new
+ timestamp = DateTime.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/time/i, time)
+ mapping.register_type(/time/i, timestamp)
+
+ assert_equal mapping.lookup('time'), timestamp
+ end
+
+ def test_fuzzy_lookup
+ string = String.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i, string)
+
+ assert_equal mapping.lookup('varchar(20)'), string
+ end
+
+ def test_aliasing_types
+ string = String.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/string/i, string)
+ mapping.alias_type(/varchar/i, 'string')
+
+ assert_equal mapping.lookup('varchar'), string
+ end
+
+ def test_changing_type_changes_aliases
+ time = Time.new
+ timestamp = DateTime.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/timestamp/i, time)
+ mapping.alias_type(/datetime/i, 'timestamp')
+ mapping.register_type(/timestamp/i, timestamp)
+
+ assert_equal mapping.lookup('datetime'), timestamp
+ end
+
+ def test_aliases_keep_metadata
+ mapping = TypeMap.new
+
+ mapping.register_type(/decimal/i) { |sql_type| sql_type }
+ mapping.alias_type(/number/i, 'decimal')
+
+ assert_equal mapping.lookup('number(20)'), 'decimal(20)'
+ assert_equal mapping.lookup('number'), 'decimal'
+ end
+
+ def test_register_proc
+ string = String.new
+ binary = Binary.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i) do |type|
+ if type.include?('(')
+ string
+ else
+ binary
+ end
+ end
+
+ assert_equal mapping.lookup('varchar(20)'), string
+ assert_equal mapping.lookup('varchar'), binary
+ end
+
+ def test_additional_lookup_args
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i) do |type, limit|
+ if limit > 255
+ 'text'
+ else
+ 'string'
+ end
+ end
+ mapping.alias_type(/string/i, 'varchar')
+
+ assert_equal mapping.lookup('varchar', 200), 'string'
+ assert_equal mapping.lookup('varchar', 400), 'text'
+ assert_equal mapping.lookup('string', 400), 'text'
+ end
+
+ def test_requires_value_or_block
+ mapping = TypeMap.new
+
+ assert_raises(ArgumentError) do
+ mapping.register_type(/only key/i)
+ end
+ end
+
+ def test_lookup_non_strings
+ mapping = HashLookupTypeMap.new
+
+ mapping.register_type(1, 'string')
+ mapping.register_type(2, 'int')
+ mapping.alias_type(3, 1)
+
+ assert_equal mapping.lookup(1), 'string'
+ assert_equal mapping.lookup(2), 'int'
+ assert_equal mapping.lookup(3), 'string'
+ assert_kind_of Type::Value, mapping.lookup(4)
+ end
+ end
+ end
+end
+
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
new file mode 100644
index 0000000000..5c54812f30
--- /dev/null
+++ b/activerecord/test/cases/types_test.rb
@@ -0,0 +1,163 @@
+require "cases/helper"
+require 'models/company'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class TypesTest < ActiveRecord::TestCase
+ def test_type_cast_boolean
+ type = Type::Boolean.new
+ assert type.type_cast_from_user('').nil?
+ assert type.type_cast_from_user(nil).nil?
+
+ assert type.type_cast_from_user(true)
+ assert type.type_cast_from_user(1)
+ assert type.type_cast_from_user('1')
+ assert type.type_cast_from_user('t')
+ assert type.type_cast_from_user('T')
+ assert type.type_cast_from_user('true')
+ assert type.type_cast_from_user('TRUE')
+ assert type.type_cast_from_user('on')
+ assert type.type_cast_from_user('ON')
+
+ # explicitly check for false vs nil
+ assert_equal false, type.type_cast_from_user(false)
+ assert_equal false, type.type_cast_from_user(0)
+ assert_equal false, type.type_cast_from_user('0')
+ assert_equal false, type.type_cast_from_user('f')
+ assert_equal false, type.type_cast_from_user('F')
+ assert_equal false, type.type_cast_from_user('false')
+ assert_equal false, type.type_cast_from_user('FALSE')
+ assert_equal false, type.type_cast_from_user('off')
+ assert_equal false, type.type_cast_from_user('OFF')
+ assert_equal false, type.type_cast_from_user(' ')
+ assert_equal false, type.type_cast_from_user("\u3000\r\n")
+ assert_equal false, type.type_cast_from_user("\u0000")
+ assert_equal false, type.type_cast_from_user('SOMETHING RANDOM')
+ end
+
+ def test_type_cast_integer
+ type = Type::Integer.new
+ assert_equal 1, type.type_cast_from_user(1)
+ assert_equal 1, type.type_cast_from_user('1')
+ assert_equal 1, type.type_cast_from_user('1ignore')
+ assert_equal 0, type.type_cast_from_user('bad1')
+ assert_equal 0, type.type_cast_from_user('bad')
+ assert_equal 1, type.type_cast_from_user(1.7)
+ assert_equal 0, type.type_cast_from_user(false)
+ assert_equal 1, type.type_cast_from_user(true)
+ assert_nil type.type_cast_from_user(nil)
+ end
+
+ def test_type_cast_non_integer_to_integer
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user([1,2])
+ assert_nil type.type_cast_from_user({1 => 2})
+ assert_nil type.type_cast_from_user((1..2))
+ end
+
+ def test_type_cast_activerecord_to_integer
+ type = Type::Integer.new
+ firm = Firm.create(:name => 'Apple')
+ assert_nil type.type_cast_from_user(firm)
+ end
+
+ def test_type_cast_object_without_to_i_to_integer
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user(Object.new)
+ end
+
+ def test_type_cast_nan_and_infinity_to_integer
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user(Float::NAN)
+ assert_nil type.type_cast_from_user(1.0/0.0)
+ end
+
+ def test_changing_integers
+ type = Type::Integer.new
+
+ assert type.changed?(5, 5, '5wibble')
+ assert_not type.changed?(5, 5, '5')
+ assert_not type.changed?(5, 5, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ def test_type_cast_float
+ type = Type::Float.new
+ assert_equal 1.0, type.type_cast_from_user("1")
+ end
+
+ def test_changing_float
+ type = Type::Float.new
+
+ assert type.changed?(5.0, 5.0, '5wibble')
+ assert_not type.changed?(5.0, 5.0, '5')
+ assert_not type.changed?(5.0, 5.0, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ def test_type_cast_binary
+ type = Type::Binary.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal "1", type.type_cast_from_user("1")
+ assert_equal 1, type.type_cast_from_user(1)
+ end
+
+ def test_type_cast_time
+ type = Type::Time.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ time_string = Time.now.utc.strftime("%T")
+ assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T")
+ end
+
+ def test_type_cast_datetime_and_timestamp
+ type = Type::DateTime.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user(' ')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ datetime_string = Time.now.utc.strftime("%FT%T")
+ assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T")
+ end
+
+ def test_type_cast_date
+ type = Type::Date.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user(' ')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ date_string = Time.now.utc.strftime("%F")
+ assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F")
+ end
+
+ def test_type_cast_duration_to_integer
+ type = Type::Integer.new
+ assert_equal 1800, type.type_cast_from_user(30.minutes)
+ assert_equal 7200, type.type_cast_from_user(2.hours)
+ end
+
+ def test_string_to_time_with_timezone
+ [:utc, :local].each do |zone|
+ with_timezone_config default: zone do
+ type = Type::DateTime.new
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
+
+ if current_adapter?(:SQLite3Adapter)
+ def test_binary_encoding
+ type = SQLite3Binary.new
+ utf8_string = "a string".encode(Encoding::UTF_8)
+ type_cast = type.type_cast_from_user(utf8_string)
+
+ assert_equal Encoding::ASCII_8BIT, type_cast.encoding
+ end
+ 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 602f633c45..e4edc437e6 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -1,44 +1,14 @@
-# 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)
- 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_associated_many
Topic.validates_associated(:replies)
Reply.validates_presence_of(:content)
@@ -94,17 +64,6 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert r.valid?
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
-
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_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index efa0c9b934..268d7914b5 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase
repair_validations(Topic, Reply)
def setup
+ repair_validations(Topic, Reply)
Reply.validates_presence_of(:title)
@topic = Topic.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
@@ -14,7 +15,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 3790d3c8cf..4f38849131 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -52,14 +52,15 @@ class PresenceValidationTest < ActiveRecord::TestCase
end
def test_validates_presence_doesnt_convert_to_array
- Speedometer.validates_presence_of :dashboard
+ speedometer = Class.new(Speedometer)
+ speedometer.validates_presence_of :dashboard
dash = Dashboard.new
# dashboard has to_a method
def dash.to_a; ['(/)', '(\)']; end
- s = Speedometer.new
+ s = speedometer.new
s.dashboard = dash
assert_nothing_raised { s.valid? }
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 74c696c858..18221cc73d 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -223,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"
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index c02b3241cd..2bbf0f23b3 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -13,7 +13,7 @@ module ActiveRecord
end
def repair_validations(*model_classes)
- yield
+ yield if block_given?
ensure
model_classes.each do |k|
k.clear_validators!
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index de618902aa..55804f9576 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -14,28 +14,28 @@ class ValidationsTest < ActiveRecord::TestCase
# Other classes we mess with will be dealt with in the specific tests
repair_validations(Topic)
- def test_error_on_create
+ def test_valid_uses_create_context_when_new
r = WrongReply.new
r.title = "Wrong Create"
- assert !r.save
+ assert_not r.valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error"
end
- def test_error_on_update
+ def test_valid_uses_update_context_when_persisted
r = WrongReply.new
r.title = "Bad"
r.content = "Good"
- assert r.save, "First save should be successful"
+ assert r.save, "First validation should be successful"
r.title = "Wrong Update"
- assert !r.save, "Second save should fail"
+ assert_not r.valid?, "Second validation should fail"
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error"
end
- def test_error_on_given_context
+ def test_valid_using_special_context
r = WrongReply.new(:title => "Valid title")
assert !r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
@@ -45,11 +45,26 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
r.author_name = nil
- assert !r.save(:context => :special_case)
+ assert_not r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
r.author_name = "secret"
- assert r.save(:context => :special_case)
+ assert r.valid?(: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
@@ -63,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal r, invalid.record
end
+ def test_validate_with_bang
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!
+ end
+ end
+
+ def test_validate_with_bang_and_context
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!(:special_case)
+ end
+ r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good")
+ assert r.validate!(:special_case)
+ end
+
def test_exception_on_create_bang_many
assert_raise(ActiveRecord::RecordInvalid) do
WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
@@ -85,7 +114,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
- def test_create_without_validation
+ def test_save_without_validation
reply = WrongReply.new
assert !reply.save
assert reply.save(:validate => false)
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 78fa2f935a..c34e7d5a30 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'
@@ -225,7 +226,6 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
written_on_in_current_timezone = topics(:first).written_on.xmlschema
- last_read_in_current_timezone = topics(:first).last_read.xmlschema
assert_equal "topic", xml.root.name
assert_equal "The First Topic" , xml.elements["//title"].text
@@ -247,14 +247,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_equal "integer", xml.elements["//parent-id"].attributes['type']
assert_equal "true", xml.elements["//parent-id"].attributes['nil']
- if current_adapter?(:SybaseAdapter)
- assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text
- assert_equal "dateTime" , xml.elements["//last-read"].attributes['type']
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
- end
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_equal "2004-04-15", xml.elements["//last-read"].text
+ assert_equal "date" , xml.elements["//last-read"].attributes['type']
# Oracle and DB2 don't have true boolean or time-only fields
unless current_adapter?(:OracleAdapter, :DB2Adapter)
@@ -421,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
def test_should_support_aliased_attributes
xml = Author.select("name as firstname").to_xml
- array = Hash.from_xml(xml)['authors']
- assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size
+ Author.all.each do |author|
+ assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml
+ end
end
def test_array_to_xml_including_has_many_association
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 15815d56e4..bce59b4fcd 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -1,8 +1,11 @@
require 'cases/helper'
require 'models/topic'
+require 'models/reply'
+require 'models/post'
+require 'models/author'
class YamlSerializationTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :authors, :posts
def test_to_yaml_with_time_with_zone_should_not_raise_exception
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
@@ -23,13 +26,6 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content)
end
- def test_encode_with_coder
- topic = Topic.first
- coder = {}
- topic.encode_with coder
- assert_equal({'attributes' => topic.attributes}, coder)
- end
-
def test_psych_roundtrip
topic = Topic.first
assert topic
@@ -47,4 +43,44 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_active_record_relation_serialization
[Topic.all].to_yaml
end
+
+ def test_raw_types_are_not_changed_on_round_trip
+ topic = Topic.new(parent_id: "123")
+ assert_equal "123", topic.parent_id_before_type_cast
+ assert_equal "123", YAML.load(YAML.dump(topic)).parent_id_before_type_cast
+ end
+
+ def test_cast_types_are_not_changed_on_round_trip
+ topic = Topic.new(parent_id: "123")
+ assert_equal 123, topic.parent_id
+ assert_equal 123, YAML.load(YAML.dump(topic)).parent_id
+ end
+
+ def test_new_records_remain_new_after_round_trip
+ topic = Topic.new
+
+ assert topic.new_record?, "Sanity check that new records are new"
+ assert YAML.load(YAML.dump(topic)).new_record?, "Record should be new after deserialization"
+
+ topic.save!
+
+ assert_not topic.new_record?, "Saved records are not new"
+ assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization"
+
+ topic = Topic.select('title').last
+
+ assert_not topic.new_record?, "Loaded records without ID are not new"
+ assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization"
+ end
+
+ def test_types_of_virtual_columns_are_not_changed_on_round_trip
+ author = Author.select('authors.*, count(posts.id) as posts_count')
+ .joins(:posts)
+ .group('authors.id')
+ .first
+ dumped = YAML.load(YAML.dump(author))
+
+ assert_equal 5, author.posts_count
+ assert_equal 5, dumped.posts_count
+ end
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index 479b8c050d..a54914c372 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -51,28 +51,6 @@ connections:
password: arunit
database: arunit2
- firebird:
- arunit:
- host: localhost
- username: rails
- password: rails
- charset: UTF8
- arunit2:
- host: localhost
- username: rails
- password: rails
- charset: UTF8
-
- frontbase:
- arunit:
- host: localhost
- username: rails
- session_name: unittest-<%= $$ %>
- arunit2:
- host: localhost
- username: rails
- session_name: unittest-<%= $$ %>
-
mysql:
arunit:
username: rails
@@ -130,11 +108,3 @@ connections:
arunit2:
adapter: sqlite3
database: ':memory:'
-
- sybase:
- arunit:
- host: database_ASE
- username: sa
- arunit2:
- host: database_ASE
- username: sa
diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml
index fb48645456..abe56752c6 100644
--- a/activerecord/test/fixtures/books.yml
+++ b/activerecord/test/fixtures/books.yml
@@ -2,8 +2,10 @@ awdr:
author_id: 1
id: 1
name: "Agile Web Development with Rails"
+ format: "paperback"
rfr:
author_id: 1
id: 2
name: "Ruby for Rails"
+ format: "ebook"
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/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml
index c93952180b..73882bac41 100644
--- a/activerecord/test/fixtures/fk_test_has_pk.yml
+++ b/activerecord/test/fixtures/fk_test_has_pk.yml
@@ -1,2 +1,2 @@
first:
- id: 1 \ No newline at end of file
+ pk_id: 1 \ No newline at end of file
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/posts.yml b/activerecord/test/fixtures/posts.yml
index 7298096025..86d46f753a 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -4,7 +4,6 @@ welcome:
title: Welcome to the weblog
body: Such a lovely day
comments_count: 2
- taggings_count: 1
tags_count: 1
type: Post
@@ -14,7 +13,6 @@ thinking:
title: So I was thinking
body: Like I hopefully always am
comments_count: 1
- taggings_count: 1
tags_count: 1
type: SpecialPost
diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml
index bf049abbf1..4c98b10380 100644
--- a/activerecord/test/fixtures/topics.yml
+++ b/activerecord/test/fixtures/topics.yml
@@ -6,7 +6,7 @@ first:
written_on: 2003-07-16t15:28:11.2233+01:00
last_read: 2004-04-15
bonus_time: 2005-01-30t15:28:00.00+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: false
replies_count: 1
@@ -15,7 +15,7 @@ second:
title: The Second Topic of the day
author_name: Mary
written_on: 2004-07-15t15:28:00.0099+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: true
replies_count: 0
parent_id: 1
@@ -26,7 +26,7 @@ third:
title: The Third Topic of the day
author_name: Carl
written_on: 2012-08-12t20:24:22.129346+00:00
- content: I'm a troll
+ content: "--- I'm a troll\n...\n"
approved: true
replies_count: 1
@@ -35,7 +35,7 @@ fourth:
title: The Fourth Topic of the day
author_name: Carl
written_on: 2006-07-15t15:28:00.0099+01:00
- content: Why not?
+ content: "--- Why not?\n...\n"
approved: true
type: Reply
parent_id: 3
@@ -45,5 +45,5 @@ fifth:
title: The Fifth Topic of the day
author_name: Jason
written_on: 2013-07-13t12:11:00.0099+01:00
- content: Omakase
+ content: "--- Omakase\n...\n"
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/models/author.rb b/activerecord/test/models/author.rb
index c197951c71..8949cf5826 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -140,6 +140,8 @@ class Author < ActiveRecord::Base
has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude'
has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments
+ has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
+
scope :relation_include_posts, -> { includes(:posts) }
scope :relation_include_tags, -> { includes(:tags) }
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/club.rb b/activerecord/test/models/club.rb
index 566e0873f1..6ceafe5858 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -6,9 +6,18 @@ class Club < ActiveRecord::Base
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
"I'm sorry sir, this is a *private* club, not a *pirate* club"
end
end
+
+class SuperClub < ActiveRecord::Base
+ self.table_name = "clubs"
+
+ has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id'
+ has_many :members, through: :memberships
+end
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/comment.rb b/activerecord/test/models/comment.rb
index ede5fbd0c6..15970758db 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -7,6 +7,9 @@ class Comment < ActiveRecord::Base
scope :created, -> { all }
belongs_to :post, :counter_cache => true
+ belongs_to :author, polymorphic: true
+ belongs_to :resource, polymorphic: true
+
has_many :ratings
belongs_to :first_post, :foreign_key => :post_id
@@ -26,6 +29,10 @@ class Comment < ActiveRecord::Base
all
end
scope :all_as_scope, -> { all }
+
+ def to_s
+ body
+ end
end
class SpecialComment < Comment
@@ -36,3 +43,11 @@ end
class VerySpecialComment < Comment
end
+
+class CommentThatAutomaticallyAltersPostBody < Comment
+ belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
+
+ after_save do |comment|
+ comment.post.update_attributes(body: "Automatically altered")
+ end
+end
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index 38b0b6aafa..dae102d12b 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -46,6 +46,24 @@ module MyApplication
end
end
end
+
+ module Suffixed
+ def self.table_name_suffix
+ '_suffixed'
+ end
+
+ class Company < ActiveRecord::Base
+ end
+
+ class Firm < Company
+ self.table_name = 'companies'
+ end
+
+ module Nested
+ class Company < ActiveRecord::Base
+ end
+ end
+ end
end
module Billing
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index a1cb8d62b6..3ea17c3abf 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -8,6 +8,7 @@ module ContactFakeColumns
table_name => 'id'
}
+ column :id, :integer
column :name, :string
column :age, :integer
column :avatar, :binary
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 2e2d8a0d37..5bd2f00129 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -13,6 +13,8 @@ class Developer < ActiveRecord::Base
end
end
+ accepts_nested_attributes_for :projects
+
has_and_belongs_to_many :projects_extended_by_name,
-> { extending(DeveloperProjectsAssociationExtension) },
:class_name => "Project",
@@ -74,12 +76,6 @@ class AuditLog < ActiveRecord::Base
belongs_to :unvalidated_developer, :class_name => 'Developer'
end
-DeveloperSalary = Struct.new(:amount)
-class DeveloperWithAggregate < ActiveRecord::Base
- self.table_name = 'developers'
- composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)]
-end
-
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
self.table_name = 'developers'
has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id'
@@ -165,6 +161,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/face.rb b/activerecord/test/models/face.rb
index edb75d333f..91e46f83e5 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,6 +1,8 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
+ # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ belongs_to :poly_man_without_inverse, :polymorphic => true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index f4d127730c..4fbb6b226b 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,6 +1,7 @@
class Man < ActiveRecord::Base
has_one :face, :inverse_of => :man
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
+ has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse
has_many :interests, :inverse_of => :man
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb
index c441be2bef..0302abad1e 100644
--- a/activerecord/test/models/movie.rb
+++ b/activerecord/test/models/movie.rb
@@ -1,3 +1,5 @@
class Movie < ActiveRecord::Base
self.primary_key = "movieid"
+
+ validates_presence_of :name
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index cf24502d3a..2e3a9a3681 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -3,6 +3,18 @@ class Owner < ActiveRecord::Base
has_many :pets, -> { order 'pets.name desc' }
has_many :toys, :through => :pets
+ belongs_to :last_pet, class_name: 'Pet'
+ scope :including_last_pet, -> {
+ select(%q[
+ owners.*, (
+ select p.pet_id from pets p
+ where p.owner_id = owners.owner_id
+ order by p.name desc
+ limit 1
+ ) as last_pet_id
+ ]).includes(:last_pet)
+ }
+
after_commit :execute_blocks
def blocks
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 8510c596a7..90a3c3ecee 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -18,7 +18,6 @@ class Pirate < ActiveRecord::Base
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'
@@ -88,8 +87,6 @@ end
class FamousPirate < ActiveRecord::Base
self.table_name = 'pirates'
-
has_many :famous_ships
-
validates_presence_of :catchphrase, on: :conference
-end
+end \ No newline at end of file
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index faf539a562..a29858213b 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -40,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
@@ -86,7 +88,7 @@ class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
- has_many :taggings, :as => :taggable
+ has_many :taggings, :as => :taggable, :counter_cache => :tags_count
has_many :tags, :through => :taggings do
def add_joins_and_select
select('tags.*, authors.id as author_id')
@@ -145,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
@@ -157,10 +167,6 @@ class Post < ActiveRecord::Base
return @log if message.nil?
@log << [message, side, new_record]
end
-
- def self.what_are_you
- 'a post...'
- end
end
class SpecialPost < Post; end
@@ -202,3 +208,12 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base
self.table_name = 'posts'
default_scope { where(:id => [1, 5,6]) }
end
+
+class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
+ self.table_name = 'posts'
+ has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id
+
+ after_save do |post|
+ post.comments.load
+ end
+end
diff --git a/activerecord/test/models/publisher.rb b/activerecord/test/models/publisher.rb
new file mode 100644
index 0000000000..0d4a7f9235
--- /dev/null
+++ b/activerecord/test/models/publisher.rb
@@ -0,0 +1,2 @@
+module Publisher
+end
diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb
new file mode 100644
index 0000000000..d73a8eb936
--- /dev/null
+++ b/activerecord/test/models/publisher/article.rb
@@ -0,0 +1,4 @@
+class Publisher::Article < ActiveRecord::Base
+ has_and_belongs_to_many :magazines
+ has_and_belongs_to_many :tags
+end
diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb
new file mode 100644
index 0000000000..82e1a14008
--- /dev/null
+++ b/activerecord/test/models/publisher/magazine.rb
@@ -0,0 +1,3 @@
+class Publisher::Magazine < ActiveRecord::Base
+ has_and_belongs_to_many :articles
+end
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 7a369b9d9a..77a4728d0b 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -20,8 +20,6 @@ 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/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/tagging.rb b/activerecord/test/models/tagging.rb
index f91f2ad2e9..a6c05da26a 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
- belongs_to :taggable, :polymorphic => true, :counter_cache => true
+ belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count
has_many :things, :through => :taggable
end
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/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index a86a188bcf..e9294a11b9 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_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_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
@@ -99,21 +99,23 @@ _SQL
_SQL
end
- if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
+ if 't' == select_value("select 'citext'=ANY(select typname from pg_type)")
execute <<_SQL
- CREATE TABLE postgresql_json_data_type (
+ CREATE TABLE postgresql_citext (
id SERIAL PRIMARY KEY,
- json_data json default '{}'::json
+ text_citext citext default ''::citext
);
_SQL
end
+ if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
execute <<_SQL
- CREATE TABLE postgresql_moneys (
+ CREATE TABLE postgresql_json_data_type (
id SERIAL PRIMARY KEY,
- wealth MONEY
+ json_data json default '{}'::json
);
_SQL
+ end
execute <<_SQL
CREATE TABLE postgresql_numbers (
@@ -141,14 +143,6 @@ _SQL
_SQL
execute <<_SQL
- CREATE TABLE postgresql_bit_strings (
- id SERIAL PRIMARY KEY,
- bit_string BIT(8),
- bit_string_varying BIT VARYING(8)
- );
-_SQL
-
- execute <<_SQL
CREATE TABLE postgresql_oids (
id SERIAL PRIMARY KEY,
obj_id OID
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 99a53434f6..a8b21904ac 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -9,13 +9,14 @@ ActiveRecord::Schema.define do
#put adapter specific setup here
case adapter_name
- # For Firebird, set the sequence values 10000 when create_table is called;
- # this prevents primary key collisions between "normally" created records
- # and fixture-based (YAML) records.
- when "Firebird"
- def create_table(*args, &block)
- ActiveRecord::Base.connection.create_table(*args, &block)
- ActiveRecord::Base.connection.execute "SET GENERATOR #{args.first}_seq TO 10000"
+ 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
@@ -53,6 +54,19 @@ ActiveRecord::Schema.define do
t.string :name
end
+ create_table :articles, force: true do |t|
+ end
+
+ create_table :articles_magazines, force: true do |t|
+ t.references :article
+ t.references :magazine
+ end
+
+ create_table :articles_tags, force: true do |t|
+ t.references :article
+ t.references :tag
+ end
+
create_table :audit_logs, force: true do |t|
t.column :message, :string, null: false
t.column :developer_id, :integer, null: false
@@ -70,6 +84,8 @@ ActiveRecord::Schema.define do
create_table :author_addresses, force: true do |t|
end
+ add_foreign_key :authors, :author_addresses
+
create_table :author_favorites, force: true do |t|
t.column :author_id, :integer
t.column :favorite_author_id, :integer
@@ -94,6 +110,7 @@ ActiveRecord::Schema.define do
create_table :books, force: true do |t|
t.integer :author_id
+ t.string :format
t.column :name, :string
t.column :status, :integer, default: 0
t.column :read_status, :integer, default: 0
@@ -161,6 +178,10 @@ ActiveRecord::Schema.define do
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
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
@@ -171,9 +192,12 @@ ActiveRecord::Schema.define do
t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, default: 0
+ t.integer :tags_count, default: 0
t.integer :children_count, default: 0
t.integer :parent_id
+ t.references :author, polymorphic: true
+ t.string :resource_id
+ t.string :resource_type
end
create_table :companies, force: true do |t|
@@ -198,6 +222,7 @@ ActiveRecord::Schema.define do
end
create_table :computers, force: true do |t|
+ t.string :system
t.integer :developer, null: false
t.integer :extendedWarranty, null: false
end
@@ -371,6 +396,9 @@ ActiveRecord::Schema.define do
t.column :custom_lock_version, :integer
end
+ create_table :magazines, force: true do |t|
+ end
+
create_table :mateys, id: false, force: true do |t|
t.column :pirate_id, :integer
t.column :target_id, :integer
@@ -508,6 +536,7 @@ ActiveRecord::Schema.define do
t.references :best_friend
t.references :best_friend_of
t.integer :insures, null: false, default: 0
+ t.timestamp :born_at
t.timestamps
end
@@ -542,7 +571,6 @@ ActiveRecord::Schema.define do
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
@@ -637,6 +665,8 @@ ActiveRecord::Schema.define do
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|
@@ -670,10 +700,14 @@ ActiveRecord::Schema.define do
end
create_table :topics, force: true do |t|
- t.string :title
+ 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
@@ -747,6 +781,8 @@ ActiveRecord::Schema.define do
t.integer :man_id
t.integer :polymorphic_man_id
t.string :polymorphic_man_type
+ t.integer :poly_man_without_inverse_id
+ t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
end
@@ -812,6 +848,8 @@ ActiveRecord::Schema.define do
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
@@ -819,12 +857,18 @@ ActiveRecord::Schema.define do
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, primary_key: "pk_id" 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'})"
+ add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
+ add_foreign_key :lessons_students, :students
+ end
- execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})"
+ create_table :overloaded_types, force: true do |t|
+ t.float :overloaded_float, default: 500
+ t.float :unoverloaded_float
+ t.string :overloaded_string_with_limit, limit: 255
+ t.string :string_with_default, default: 'the original default'
end
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index b7aff4f47d..b5552c2755 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -7,7 +7,7 @@ ActiveRecord::Schema.define do
execute "DROP TABLE fk_test_has_pk" rescue nil
execute <<_SQL
CREATE TABLE 'fk_test_has_pk' (
- 'id' INTEGER NOT NULL PRIMARY KEY
+ 'pk_id' INTEGER NOT NULL PRIMARY KEY
);
_SQL
@@ -16,7 +16,7 @@ _SQL
'id' INTEGER NOT NULL PRIMARY KEY,
'fk_id' INTEGER NOT NULL,
- FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id')
+ FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id')
);
_SQL
-end \ No newline at end of file
+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..43cb235e01
--- /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.execute("CREATE TABLE #{table_name}(#{definition})")
+ yield
+ ensure
+ connection.execute("DROP TABLE #{table_name}")
+ end
+end
diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb
new file mode 100644
index 0000000000..2ae8d299e5
--- /dev/null
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -0,0 +1,11 @@
+module SchemaDumpingHelper
+ def dump_table_schema(table, connection = ActiveRecord::Base.connection)
+ old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table]
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ stream.string
+ ensure
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
+ end
+end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 43bfeff079..b961b373cb 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,617 +1,293 @@
-* Fix the implementation of Multibyte::Unicode.tidy_bytes for JRuby
+* Fix ActiveSupport::TestCase not to order users' test cases by default.
+ If this change breaks your tests because your tests are order dependent, you need to explicitly call
+ ActiveSupport::TestCase.my_tests_are_order_dependent! at the top of your tests.
- The existing implementation caused JRuby to raise the error:
- `Encoding::ConverterNotFoundError: code converter not found (UTF-8 to UTF8-MAC)`
+ *Akira Matsuda*
- *Justin Coyne*
+* Fix DateTime comparison with DateTime::Infinity object.
-* Fix `to_param` behavior when there are nested empty hashes.
-
- Before:
-
- params = {c: 3, d: {}}.to_param # => "&c=3"
-
- After:
-
- params = {c: 3, d: {}}.to_param # => "c=3&d="
-
- Fixes #13892.
-
- *Hincu Petru*
-
-* Deprecate custom `BigDecimal` serialization.
-
- Deprecate the custom `BigDecimal` serialization that is included when requiring
- `active_support/all` as a fix for #12467. Let Ruby handle YAML serialization
- for `BigDecimal` instead.
-
- *David Celis*
-
-* Fix parsing bugs in `XmlMini`
-
- Symbols or boolean parsing would raise an error for non string values (e.g.
- integers). Decimal parsing would fail due to a missing requirement.
-
- *Birkir A. Barkarson*
-
-* Maintain the current timezone when calling `wrap_with_time_zone`
+ *Rafael Mendonça França*
- Extend the solution from the fix for #12163 to the general case where `Time`
- methods are wrapped with a time zone.
+* Added Object#itself which returns the object itself. Useful when dealing with a chaining scenario, like Active Record scopes:
- Fixes #12596.
+ Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at)
- *Andrew White*
+ *DHH*
-* Remove behavior that automatically remove the Date/Time stubs, added by `travel`
- and `travel_to` methods, after each test case.
+* `Object#with_options` executes block in merging option context when
+ explicit receiver in not passed.
- Now users have to use the `travel_back` or the block version of `travel` and
- `travel_to` methods to clean the stubs.
+ *Pavel Pravosud*
- *Rafael Mendonça França*
+* Fixed a compatibility issue with the `Oj` gem when cherry-picking the file
+ `active_support/core_ext/object/json` without requiring `active_support/json`.
-* Add `travel_back` to remove stubs from `travel` and `travel_to`.
+ Fixes #16131.
- *Rafael Mendonça França*
+ *Godfrey Chan*
-* Remove the deprecation about the `#filter` method.
+* Make `Hash#with_indifferent_access` copy the default proc too.
- Filter objects should now rely on method corresponding to the filter type
- (e.g. `#before`).
+ *arthurnn*, *Xanders*
- *Aaron Patterson*
+* Add `String#truncate_words` to truncate a string by a number of words.
-* Add `ActiveSupport::JSON::Encoding.time_precision` as a way to configure the
- precision of encoded time values:
+ *Mohamed Osama*
- Time.utc(2000, 1, 1).as_json # => "2000-01-01T00:00:00.000Z"
- ActiveSupport::JSON::Encoding.time_precision = 0
- Time.utc(2000, 1, 1).as_json # => "2000-01-01T00:00:00Z"
+* Deprecate `capture` and `quietly`.
- *Parker Selbert*
+ These methods are not thread safe and may cause issues when used in threaded environments.
+ To avoid problems we are deprecating them.
-* Maintain the current timezone when calling `change` during DST overlap
+ *Tom Meier*
- Currently if a time is changed during DST overlap in the autumn then the method
- `period_for_local` will return the DST period. However if the original time is
- not DST then this can be surprising and is not what is generally wanted. This
- commit changes that behavior to maintain the current period if it's in the list
- of periods returned by `periods_for_local`.
+* `DateTime#to_f` now preserves the fractional seconds instead of always
+ rounding to `.0`.
- Fixes #12163.
+ Fixes #15994.
- *Andrew White*
+ *John Paul Ashenfelter*
-* Added `Hash#compact` and `Hash#compact!` for removing items with nil value
- from hash.
+* Add `Hash#transform_values` to simplify a common pattern where the values of a
+ hash must change, but the keys are left the same.
- *Celestino Gomes*
+ *Sean Griffin*
-* Maintain proleptic gregorian in Time#advance
+* Always instrument `ActiveSupport::Cache`.
- `Time#advance` uses `Time#to_date` and `Date#advance` to calculate a new date.
- The `Date` object returned by `Time#to_date` is constructed with the assumption
- that the `Time` object represents a proleptic gregorian date, but it is
- configured to observe the default julian calendar reform date (2299161j)
- for purposes of calculating month, date and year:
+ Since `ActiveSupport::Notifications` only instrument items when there
+ are subscriber we don't need to disable instrumentation.
- Time.new(1582, 10, 4).to_date.to_s # => "1582-09-24"
- Time.new(1582, 10, 4).to_date.gregorian.to_s # => "1582-10-04"
+ *Peter Wagenet*
- This patch ensures that when the intermediate `Date` object is advanced
- to yield a new `Date` object, that the `Time` object for return is constructed
- with a proleptic gregorian month, date and year.
+* Make the `apply_inflections` method case-insensitive when checking
+ whether a word is uncountable or not.
- *Riley Lynch*
+ *Robin Dupret*
-* `MemCacheStore` should only accept a `Dalli::Client`, or create one.
+* Make Dependencies pass a name to NameError error.
*arthurnn*
-* Don't lazy load the `tzinfo` library as it causes problems on Windows.
+* Fixed `ActiveSupport::Cache::FileStore` exploding with long paths.
- Fixes #13553.
+ *Adam Panzer / Michael Grosser*
- *Andrew White*
+* Fixed `ActiveSupport::TimeWithZone#-` so precision is not unnecessarily lost
+ when working with objects with a nanosecond component.
-* Use `remove_possible_method` instead of `remove_method` to avoid
- a `NameError` to be thrown on FreeBSD with the `Date` object.
+ `ActiveSupport::TimeWithZone#-` should return the same result as if we were
+ using `Time#-`:
- *Rafael Mendonça França*, *Robin Dupret*
-
-* `blank?` and `present?` commit to return singletons.
-
- *Xavier Noria*, *Pavel Pravosud*
-
-* Fixed Float related error in NumberHelper with large precisions.
+ Time.now.end_of_day - Time.now.beginning_of_day #=> 86399.999999999
Before:
- ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50
- #=> "3.14158999999999988261834005243144929409027099609375"
+ Time.zone.now.end_of_day.nsec #=> 999999999
+ Time.zone.now.end_of_day - Time.zone.now.beginning_of_day #=> 86400.0
After:
- ActiveSupport::NumberHelper.number_to_rounded '3.14159', precision: 50
- #=> "3.14159000000000000000000000000000000000000000000000"
-
- *Kenta Murata*, *Akira Matsuda*
-
-* 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.
-
- To disable it add the following configuration to your application:
-
- config.i18n.enforce_available_locales = false
-
- This also ensures I18n configuration is properly initialized taking the new
- option into account, to avoid their deprecations while booting up the app.
-
- *Carlos Antonio da Silva*, *Yves Senn*
-
-* Introduce Module#concerning: a natural, low-ceremony way to separate
- responsibilities within a class.
-
- Imported from https://github.com/37signals/concerning#readme
-
- class Todo < ActiveRecord::Base
- concerning :EventTracking do
- included do
- has_many :events
- end
-
- def latest_event
- ...
- end
-
- private
- def some_internal_method
- ...
- end
- end
-
- concerning :Trashable do
- def trashed?
- ...
- end
-
- def latest_event
- super some_option: true
- end
- end
- end
-
- is equivalent to defining these modules inline, extending them into
- concerns, then mixing them in to the class.
-
- Inline concerns tame "junk drawer" classes that intersperse many unrelated
- class-level declarations, public instance methods, and private
- implementation. Coalesce related bits and give them definition.
- These are a stepping stone toward future growth & refactoring.
-
- When to move on from an inline concern:
- * Encapsulating state? Extract collaborator object.
- * Encompassing more public behavior or implementation? Move to separate file.
- * Sharing behavior among classes? Move to separate file.
-
- *Jeremy Kemper*
-
-* Fix file descriptor being leaked on each call to `Kernel.silence_stream`.
-
- *Mario Visic*
-
-* Added `Date#all_week/month/quarter/year` for generating date ranges.
-
- *Dmitriy Meremyanin*
+ Time.zone.now.end_of_day - Time.zone.now.beginning_of_day
+ #=> 86399.999999999
-* Add `Time.zone.yesterday` and `Time.zone.tomorrow`. These follow the
- behavior of Ruby's `Date.yesterday` and `Date.tomorrow` but return localized
- versions, similar to how `Time.zone.today` has returned a localized version
- of `Date.today`.
+ *Gordon Chan*
- *Colin Bartlett*
+* Fixed precision error in NumberHelper when using Rationals.
-* Show valid keys when `assert_valid_keys` raises an exception, and show the
- wrong value as it was entered.
-
- *Gonzalo Rodríguez-Baltanás Díaz*
-
-* Both `cattr_*` and `mattr_*` method definitions now live in `active_support/core_ext/module/attribute_accessors`.
-
- Requires to `active_support/core_ext/class/attribute_accessors` are
- deprecated and will be removed in Ruby on Rails 4.2.
-
- *Genadi Samokovarov*
-
-* 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`
-
- This will help to catch subtle bugs like:
-
- def recent?(days = 3)
- self.created_at >= days.ago
- end
+ Before:
- The above code would check if the model is created within the last 3 **seconds**.
+ ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2
+ #=> "330.00"
- In the future, `Numeric#{ago,until,since,from_now}` should be removed completely,
- or throw some sort of errors to indicate there are no implicit conversion from
- Numeric to AS::Duration.
+ After:
- *Godfrey Chan*
+ ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2
+ #=> "333.33"
-* Requires JSON gem version 1.7.7 or above due to a security issue in older versions.
+ See #15379.
- *Godfrey Chan*
+ *Juanjo Bazán*
-* Removed the old pure-Ruby JSON encoder and switched to a new encoder based on the built-in JSON
- gem.
+* Removed deprecated `Numeric#ago` and friends
- Support for encoding `BigDecimal` as a JSON number, as well as defining custom `encode_json`
- methods to control the JSON output has been **removed from core**. The new encoder will always
- encode BigDecimals as `String`s and ignore any custom `encode_json` methods.
+ Replacements:
- The old encoder has been extracted into the `activesupport-json_encoder` gem. Installing that
- gem will bring back the ability to encode `BigDecimal`s as numbers as well as `encode_json`
- support.
+ 5.ago => 5.seconds.ago
+ 5.until => 5.seconds.until
+ 5.since => 5.seconds.since
+ 5.from_now => 5.seconds.from_now
- Setting the related configuration `ActiveSupport.encode_big_decimal_as_string` without the
- `activesupport-json_encoder` gem installed will raise an error.
+ See #12389 for the history and rationale behind this.
*Godfrey Chan*
-* Add `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These methods change current
- time to the given time or time difference by stubbing `Time.now` and `Date.today` to return the
- time or date after the difference calculation, or the time or date that got passed into the
- method respectively.
-
- Example for `#travel`:
-
- Time.now # => 2013-11-09 15:34:49 -05:00
- travel 1.day
- Time.now # => 2013-11-10 15:34:49 -05:00
- Date.today # => Sun, 10 Nov 2013
-
- Example for `#travel_to`:
-
- Time.now # => 2013-11-09 15:34:49 -05:00
- travel_to Time.new(2004, 11, 24, 01, 04, 44)
- Time.now # => 2004-11-24 01:04:44 -05:00
- Date.today # => Wed, 24 Nov 2004
+* DateTime `advance` now supports partial days.
- Both of these methods also accept a block, which will return the current time back to its
- original state at the end of the block:
-
- Time.now # => 2013-11-09 15:34:49 -05:00
-
- travel 1.day do
- User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
- end
-
- travel_to Time.new(2004, 11, 24, 01, 04, 44) do
- User.create.created_at # => Wed, 24 Nov 2004 01:04:44 EST -05:00
- end
-
- Time.now # => 2013-11-09 15:34:49 -05:00
-
- This module is included in `ActiveSupport::TestCase` automatically.
-
- *Prem Sichanugrist*, *DHH*
+ Before:
-* Unify `cattr_*` interface: allow to pass a block to `cattr_reader`.
+ DateTime.now.advance(days: 1, hours: 12)
- Example:
+ After:
- class A
- cattr_reader(:defr) { 'default_reader_value' }
- end
- A.defr # => 'default_reader_value'
+ DateTime.now.advance(days: 1.5)
- *Alexey Chernenkov*
+ Fixes #12005.
-* Improved compatibility with the stdlib JSON gem.
+ *Shay Davidson*
- Previously, calling `::JSON.{generate,dump}` sometimes causes unexpected
- failures such as intridea/multi_json#86.
+* `Hash#deep_transform_keys` and `Hash#deep_transform_keys!` now transform hashes
+ in nested arrays. This change also applies to `Hash#deep_stringify_keys`,
+ `Hash#deep_stringify_keys!`, `Hash#deep_symbolize_keys` and
+ `Hash#deep_symbolize_keys!`.
- `::JSON.{generate,dump}` now bypasses the ActiveSupport JSON encoder
- completely and yields the same result with or without ActiveSupport. This
- means that it will **not** call `as_json` and will ignore any options that
- the JSON gem does not natively understand. To invoke ActiveSupport's JSON
- encoder instead, use `obj.to_json(options)` or
- `ActiveSupport::JSON.encode(obj, options)`.
+ *OZAWA Sakuro*
- *Godfrey Chan*
+* Fixed confusing `DelegationError` in `Module#delegate`.
-* Fix Active Support `Time#to_json` and `DateTime#to_json` to return 3 decimal
- places worth of fractional seconds, similar to `TimeWithZone`.
+ See #15186.
- *Ryan Glover*
+ *Vladimir Yarotsky*
-* Removed circular reference protection in JSON encoder, deprecated
- `ActiveSupport::JSON::Encoding::CircularReferenceError`.
+* Fixed `ActiveSupport::Subscriber` so that no duplicate subscriber is created
+ when a subscriber method is redefined.
- *Godfrey Chan*, *Sergio Campamá*
+ *Dennis Schön*
-* Add `capitalize` option to `Inflector.humanize`, so strings can be humanized without being capitalized:
+* Remove deprecated string based terminators for `ActiveSupport::Callbacks`.
- 'employee_salary'.humanize # => "Employee salary"
- 'employee_salary'.humanize(capitalize: false) # => "employee salary"
+ *Eileen M. Uchitelle*
- *claudiob*
+* Fixed an issue when using
+ `ActiveSupport::NumberHelper::NumberToDelimitedConverter` to
+ convert a value that is an `ActiveSupport::SafeBuffer` introduced
+ in 2da9d67.
-* Fixed `Object#as_json` and `Struct#as_json` not working properly with options. They now take
- the same options as `Hash#as_json`:
+ See #15064.
- struct = Struct.new(:foo, :bar).new
- struct.foo = "hello"
- struct.bar = "world"
- json = struct.as_json(only: [:foo]) # => {foo: "hello"}
+ *Mark J. Titorenko*
- *Sergio Campamá*, *Godfrey Chan*
+* `TimeZone#parse` defaults the day of the month to '1' if any other date
+ components are specified. This is more consistent with the behavior of
+ `Time#parse`.
-* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed them to JavaScript functions like `getTime()`.
+ *Ulysse Carion*
- *DHH*
+* `humanize` strips leading underscores, if any.
-* Calling `ActiveSupport::JSON.decode` with unsupported options now raises an error.
+ Before:
- *Godfrey Chan*
+ '_id'.humanize # => ""
-* Support `:unless_exist` in `FileStore`.
+ After:
- *Michael Grosser*
+ '_id'.humanize # => "Id"
-* Fix `slice!` deleting the default value of the hash.
+ *Xavier Noria*
- *Antonio Santos*
+* Fixed backward compatibility isues introduced in 326e652.
-* `require_dependency` accepts objects that respond to `to_path`, in
- particular `Pathname` instances.
+ Empty Hash or Array should not present in serialization result.
- *Benjamin Fleischer*
+ {a: []}.to_query # => ""
+ {a: {}}.to_query # => ""
-* Disable the ability to iterate over Range of AS::TimeWithZone
- due to significant performance issues.
+ For more info see #14948.
*Bogdan Gusiev*
-* Allow attaching event subscribers to ActiveSupport::Notifications namespaces
- before they're defined. Essentially, this means instead of this:
+* Add `Digest::UUID::uuid_v3` and `Digest::UUID::uuid_v5` to support stable
+ UUID fixtures on PostgreSQL.
- class JokeSubscriber < ActiveSupport::Subscriber
- def sql(event)
- puts "A rabbi and a priest walk into a bar..."
- end
+ *Roderick van Domburg*
- # This call needs to happen *after* defining the methods.
- attach_to "active_record"
- end
+* Fixed `ActiveSupport::Duration#eql?` so that `1.second.eql?(1.second)` is
+ true.
- You can do this:
+ This fixes the current situation of:
- class JokeSubscriber < ActiveSupport::Subscriber
- # This is much easier to read!
- attach_to "active_record"
+ 1.second.eql?(1.second) #=> false
- def sql(event)
- puts "A rabbi and a priest walk into a bar..."
- end
- end
-
- This should make it easier to read and understand these subscribers.
+ `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:
- *Daniel Schierbeck*
+ 1.eql?(1.0) #=> false
+ 1.0.eql?(1) #=> false
-* Add `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` methods.
+ 1.second.eql?(1) #=> false (was true)
+ 1.eql?(1.second) #=> false
- Also added `midday`, `noon`, `at_midday`, `at_noon` and `at_middle_of_day` as aliases.
+ { 1 => "foo", 1.0 => "bar" }
+ #=> { 1 => "foo", 1.0 => "bar" }
- *Anatoli Makarevich*
+ { 1 => "foo", 1.second => "bar" }
+ # now => { 1 => "foo", 1.second => "bar" }
+ # was => { 1 => "bar" }
-* Fix ActiveSupport::Cache::FileStore#cleanup to no longer rely on missing each_key method.
+ And though the behavior of these hasn't changed, for reference:
- *Murray Steele*
+ 1 == 1.0 #=> true
+ 1.0 == 1 #=> true
-* Ensure that autoloaded constants in all-caps nestings are marked as
- autoloaded.
+ 1 == 1.second #=> true
+ 1.second == 1 #=> true
- *Simon Coffey*
+ *Emily Dobervich*
-* Add `String#remove(pattern)` as a short-hand for the common pattern of
- `String#gsub(pattern, '')`.
+* `ActiveSupport::SafeBuffer#prepend` acts like `String#prepend` and modifies
+ instance in-place, returning self. `ActiveSupport::SafeBuffer#prepend!` is
+ deprecated.
- *DHH*
+ *Pavel Pravosud*
-* Adds a new deprecation behaviour that raises an exception. Throwing this
- line into +config/environments/development.rb+
+* `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.
- ActiveSupport::Deprecation.behavior = :raise
+ *Peter Jaros*
- will cause the application to raise an +ActiveSupport::DeprecationException+
- on deprecations.
+* Deprecate `Class#superclass_delegating_accessor`, use `Class#class_attribute` instead.
- Use this for aggressive deprecation cleanups.
-
- *Xavier Noria*
+ *Akshay Vishnoi*
-* Remove 'cow' => 'kine' irregular inflection from default inflections.
+* Ensure classes which `include Enumerable` get `#to_json` in addition to
+ `#as_json`.
- *Andrew White*
+ *Sammy Larbi*
-* Add `DateTime#to_s(:iso8601)` and `Date#to_s(:iso8601)` for consistency.
+* Change the signature of `fetch_multi` to return a hash rather than an
+ array. This makes it consistent with the output of `read_multi`.
- *Andrew White*
+ *Parker Selbert*
-* Add `Time#to_s(:iso8601)` for easy conversion of times to the iso8601 format for easy Javascript date parsing.
+* 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.
- *DHH*
+ # app/models/concerns/authentication.rb
+ concern :Authentication do
+ included do
+ after_create :generate_private_key
+ end
-* Improve `ActiveSupport::Cache::MemoryStore` cache size calculation.
- The memory used by a key/entry pair is calculated via `#cached_size`:
+ class_methods do
+ def authenticate(credentials)
+ # ...
+ end
+ end
- def cached_size(key, entry)
- key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
+ def generate_private_key
+ # ...
+ end
end
- The value of `PER_ENTRY_OVERHEAD` is 240 bytes based on an [empirical
- estimation](https://gist.github.com/ssimeonov/6047200) for 64-bit MRI on
- 1.9.3 and 2.0.
-
- Fixes #11512.
-
- *Simeon Simeonov*
-
-* Only raise `Module::DelegationError` if it's the source of the exception.
-
- Fixes #10559.
-
- *Andrew White*
-
-* Make `Time.at_with_coercion` retain the second fraction and return local time.
-
- Fixes #11350.
-
- *Neer Friedman*, *Andrew White*
-
-* Make `HashWithIndifferentAccess#select` always return the hash, even when
- `Hash#select!` returns `nil`, to allow further chaining.
-
- *Marc Schütz*
-
-* Remove deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`).
-
- *Arun Agrawal*
-
-* Remove deprecated `Module#local_constant_names` in favor of `Module#local_constants`.
-
- *Arun Agrawal*
-
-* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`.
-
- *Arun Agrawal*
-
-* Remove deprecated `Logger` core extensions (`core_ext/logger.rb`).
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated `Time#time_with_datetime_fallback`, `Time#utc_time`
- and `Time#local_time` in favor of `Time#utc` and `Time#local`.
-
- *Vipul A M*
-
-* Remove deprecated `Hash#diff` with no replacement.
-
- If you're using it to compare hashes for the purpose of testing, please use
- MiniTest's `assert_equal` instead.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`.
-
- *Vipul A M*
-
-* Remove deprecated `Proc#bind` with no replacement.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated `Array#uniq_by` and `Array#uniq_by!`, use native
- `Array#uniq` and `Array#uniq!` instead.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated `ActiveSupport::BasicObject`, use `ActiveSupport::ProxyObject` instead.
-
- *Carlos Antonio da Silva*
-
-* Remove deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead.
-
- *Yves Senn*
-
-* Remove deprecated `assert_present` and `assert_blank` methods, use `assert
- object.blank?` and `assert object.present?` instead.
-
- *Yves Senn*
-
-* Fix return value from `BacktraceCleaner#noise` when the cleaner is configured
- with multiple silencers.
-
- Fixes #11030.
-
- *Mark J. Titorenko*
-
-* `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess`
- instance instead of a `Hash` instance.
-
- Fixes #10723.
-
- *Albert Llop*
-
-* Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps
- sub-second resolution when wrapping a `DateTime` value.
-
- Fixes #10855.
-
- *Andrew White*
-
-* Fix `ActiveSupport::Dependencies::Loadable#load_dependency` calling
- `#blame_file!` on Exceptions that do not have the Blamable mixin
-
- *Andrew Kreiling*
-
-* Override `Time.at` to support the passing of Time-like values when called with a single argument.
-
- *Andrew White*
-
-* Prevent side effects to hashes inside arrays when
- `Hash#with_indifferent_access` is called.
-
- Fixes #10526.
-
- *Yves Senn*
-
-* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement.
-
- *Toshinori Kajihara*
-
-* 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.
-
- *Mike Dillon*
-
-* Replace `multi_json` with `json`.
-
- Since Rails requires Ruby 1.9 and since Ruby 1.9 includes `json` in the standard library,
- `multi_json` is no longer necessary.
-
- *Erik Michaels-Ober*
-
-* 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.
-
- *Mario Caropreso + Viktor Kelemen + zackham*
-
-* Fix skipping object callbacks using metadata fetched via callback chain
- inspection methods (`_*_callbacks`)
-
- *Sean Walbran*
-
-* Add a `fetch_multi` method to the cache stores. The method provides
- an easy to use API for fetching multiple values from the cache.
-
- Example:
-
- # 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)
+ # 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/README.rdoc b/activesupport/README.rdoc
index f3582767c0..a6424a353a 100644
--- a/activesupport/README.rdoc
+++ b/activesupport/README.rdoc
@@ -30,6 +30,11 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index f3625e8b79..c0b457c341 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
s.rdoc_options.concat ['--encoding', 'UTF-8']
- s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9'
+ s.add_dependency 'i18n', '>= 0.7.0.dev', '< 0.8'
s.add_dependency 'json', '~> 1.7', '>= 1.7.7'
s.add_dependency 'tzinfo', '~> 1.1'
s.add_dependency 'minitest', '~> 5.1'
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index d58578b7bc..d06f22ad5c 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -13,7 +13,7 @@ module ActiveSupport
# can focus on the rest.
#
# bc = BacktraceCleaner.new
- # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix
+ # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
@@ -65,14 +65,14 @@ module ActiveSupport
@silencers << block
end
- # Will remove all silencers, but leave in the filters. This is useful if
- # your context of debugging suddenly expands as you suspect a bug in one of
+ # Removes all silencers, but leaves in the filters. Useful if your
+ # context of debugging suddenly expands as you suspect a bug in one of
# the libraries you use.
def remove_silencers!
@silencers = []
end
- # Removes all filters, but leaves in silencers. Useful if you suddenly
+ # Removes all filters, but leaves in the silencers. Useful if you suddenly
# need to see entire filepaths in the backtrace that you had already
# filtered out.
def remove_filters!
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 2b7f5943b5..ff67a6828c 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -8,6 +8,7 @@ require 'active_support/core_ext/numeric/bytes'
require 'active_support/core_ext/numeric/time'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/string/inflections'
+require 'active_support/deprecation'
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
@@ -178,14 +179,16 @@ module ActiveSupport
@silence = previous_silence
end
- # Set to +true+ if cache stores should be instrumented.
- # Default is +false+.
+ # :deprecated:
def self.instrument=(boolean)
- Thread.current[:instrument_cache_store] = boolean
+ ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument= is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it."
+ true
end
+ # :deprecated:
def self.instrument
- Thread.current[:instrument_cache_store] || false
+ ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it."
+ true
end
# Fetches data from the cache, using the given key. If there is data in
@@ -234,7 +237,7 @@ module ActiveSupport
# seconds. Because of extended life of the previous cache, other processes
# 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.
+ # new value. After that all the processes will start getting the new value.
# The key is to keep <tt>:race_condition_ttl</tt> small.
#
# If the process regenerating the entry errors out, the entry will be
@@ -357,20 +360,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
@@ -540,13 +542,9 @@ module ActiveSupport
def instrument(operation, key, options = nil)
log(operation, key, options)
- if self.class.instrument
- payload = { :key => key }
- payload.merge!(options) if options.is_a?(Hash)
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
- else
- yield(nil)
- end
+ payload = { :key => key }
+ payload.merge!(options) if options.is_a?(Hash)
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
end
def log(operation, key, options = nil)
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 8ed60aebac..d08ecd2f7d 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -14,6 +14,7 @@ module ActiveSupport
DIR_FORMATTER = "%03X"
FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
+ FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
EXCLUDED_DIRS = ['.', '..'].freeze
def initialize(cache_path, options = nil)
@@ -117,6 +118,10 @@ module ActiveSupport
# Translate a key into a file path.
def key_file_path(key)
+ if key.size > FILEPATH_MAX_SIZE
+ key = Digest::MD5.hexdigest(key)
+ end
+
fname = URI.encode_www_form_component(key)
hash = Zlib.adler32(fname)
hash, dir_1 = hash.divmod(0x1000)
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index 4eaf57f385..73c6b3cb88 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,6 +1,6 @@
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/string/inflections'
-require 'rack/body_proxy'
+require 'active_support/per_thread_registry'
module ActiveSupport
module Cache
@@ -9,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
@@ -64,37 +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)
- 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
-
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
def middleware
@@ -115,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
@@ -149,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
@@ -162,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 e14ece7f35..cd467e13f6 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -71,7 +71,8 @@ module ActiveSupport
# order.
#
# If the callback chain was halted, returns +false+. Otherwise returns the
- # result of the block, or +true+ if no block is given.
+ # result of the block, +nil+ if no callbacks have been set, or +true+
+ # if callbacks have been set but no block is given.
#
# run_callbacks :save do
# save
@@ -131,8 +132,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
@@ -149,6 +148,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|
@@ -166,6 +166,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :halting
def self.conditional(next_callback, user_callback, user_conditions)
lambda { |env|
@@ -178,6 +179,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :conditional
def self.simple(next_callback, user_callback)
lambda { |env|
@@ -185,6 +187,7 @@ module ActiveSupport
next_callback.call env
}
end
+ private_class_method :simple
end
class After
@@ -208,8 +211,6 @@ module ActiveSupport
end
end
- private
-
def self.halting_and_conditional(next_callback, user_callback, user_conditions)
lambda { |env|
env = next_callback.call env
@@ -223,6 +224,7 @@ module ActiveSupport
env
}
end
+ private_class_method :halting_and_conditional
def self.halting(next_callback, user_callback)
lambda { |env|
@@ -233,6 +235,7 @@ module ActiveSupport
env
}
end
+ private_class_method :halting
def self.conditional(next_callback, user_callback, user_conditions)
lambda { |env|
@@ -246,6 +249,7 @@ module ActiveSupport
env
}
end
+ private_class_method :conditional
def self.simple(next_callback, user_callback)
lambda { |env|
@@ -254,6 +258,7 @@ module ActiveSupport
env
}
end
+ private_class_method :simple
end
class Around
@@ -269,8 +274,6 @@ module ActiveSupport
end
end
- private
-
def self.halting_and_conditional(next_callback, user_callback, user_conditions)
lambda { |env|
target = env.target
@@ -288,23 +291,25 @@ module ActiveSupport
end
}
end
+ private_class_method :halting_and_conditional
def self.halting(next_callback, user_callback)
lambda { |env|
target = env.target
value = env.value
- unless env.halted
+ if env.halted
+ next_callback.call env
+ else
user_callback.call(target, value) {
env = next_callback.call env
env.value
}
env
- else
- next_callback.call env
end
}
end
+ private_class_method :halting
def self.conditional(next_callback, user_callback, user_conditions)
lambda { |env|
@@ -322,6 +327,7 @@ module ActiveSupport
end
}
end
+ private_class_method :conditional
def self.simple(next_callback, user_callback)
lambda { |env|
@@ -332,6 +338,7 @@ module ActiveSupport
env
}
end
+ private_class_method :simple
end
end
@@ -409,15 +416,8 @@ module ActiveSupport
# Procs:: A proc to call with the object.
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
#
- # All of these objects are compiled into methods and handled
- # the same after this point:
- #
- # Symbols:: Already methods.
- # Strings:: class_eval'd into methods.
- # Procs:: using define_method compiled into methods.
- # Objects::
- # a method is created that calls the before_foo method
- # on the object.
+ # All of these objects are converted into a lambda and handled
+ # the same after this point.
def make_lambda(filter)
case filter
when Symbol
@@ -566,7 +566,7 @@ module ActiveSupport
#
# set_callback :save, :before, :before_meth
# set_callback :save, :after, :after_meth, if: :condition
- # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+ # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
#
# The second arguments indicates whether the callback is to be run +:before+,
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
@@ -718,12 +718,6 @@ module ActiveSupport
# would call <tt>Audit#save</tt>.
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 = options[:terminator]
- line = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__
- options[:terminator] = lambda { |target, result| target.instance_exec(result, &line) }
- end
names.each do |name|
class_attribute "_#{name}_callbacks"
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index b796d01dfd..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
@@ -130,5 +130,13 @@ module ActiveSupport
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/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 67f58bc0fe..caa499dfa2 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -5,6 +5,8 @@ class Array
# %w( a b c d ).from(2) # => ["c", "d"]
# %w( a b c d ).from(10) # => []
# %w().from(0) # => []
+ # %w( a b c d ).from(-2) # => ["c", "d"]
+ # %w( a b c ).from(-10) # => []
def from(position)
self[position, length] || []
end
@@ -15,8 +17,10 @@ class Array
# %w( a b c d ).to(2) # => ["a", "b", "c"]
# %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
# %w().to(0) # => []
+ # %w( a b c d ).to(-2) # => ["a", "b", "c"]
+ # %w( a b c ).to(-10) # => []
def to(position)
- first position + 1
+ self[0..position]
end
# Equal to <tt>self[1]</tt>.
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index 3529d57174..87ae052eb0 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -18,6 +18,11 @@ class Array
# ["3", "4"]
# ["5"]
def in_groups_of(number, fill_with = nil)
+ if number.to_i <= 0
+ raise ArgumentError,
+ "Group size must be a positive integer, was #{number.inspect}"
+ end
+
if fill_with == false
collection = self
else
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 083b165dce..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,6 +1,4 @@
-require 'active_support/deprecation'
+# 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'
-
-ActiveSupport::Deprecation.warn(
- "The cattr_* method definitions have been moved into active_support/core_ext/module/attribute_accessors. Please require that instead."
-)
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 c2219beb5a..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,5 +1,7 @@
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 = {})
@@ -21,6 +23,8 @@ class Class
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_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 73ad0aa097..dc4e767e9d 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -53,6 +53,16 @@ class DateTime
# <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)
+ options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
+ end
+
+ unless options[:days].nil?
+ options[:days], partial_days = options[:days].divmod(1)
+ options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
+ end
+
d = to_date.advance(options)
datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
seconds_to_advance = \
@@ -63,7 +73,7 @@ class DateTime
if seconds_to_advance.zero?
datetime_advanced_by_date
else
- datetime_advanced_by_date.since seconds_to_advance
+ datetime_advanced_by_date.since(seconds_to_advance)
end
end
@@ -151,7 +161,9 @@ class DateTime
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
def <=>(other)
- if other.respond_to? :to_datetime
+ if other.kind_of?(Infinity)
+ super
+ elsif other.respond_to? :to_datetime
super other.to_datetime
else
nil
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 6ddfb72a0d..2a9c09fc29 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -71,9 +71,9 @@ class DateTime
civil(year, month, day, hour, min, sec, offset)
end
- # Converts +self+ to a floating-point number of seconds since the Unix epoch.
+ # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch.
def to_f
- seconds_since_unix_epoch.to_f
+ seconds_since_unix_epoch.to_f + sec_fraction
end
# Converts +self+ to an integer number of seconds since the Unix epoch.
diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb
new file mode 100644
index 0000000000..593c51bba2
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb
@@ -0,0 +1,51 @@
+require 'securerandom'
+
+module Digest
+ module UUID
+ DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ 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
+
+ # Convenience method for SecureRandom.uuid.
+ def self.uuid_v4
+ SecureRandom.uuid
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index f68e1662f9..af4d1da0eb 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -6,3 +6,4 @@ require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 7bea461c77..2149d4439d 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -105,7 +105,7 @@ class Hash
# hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
#
- # DisallowedType is raised 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
@@ -221,7 +221,7 @@ module ActiveSupport
def garbage?(value)
# If the type is the only element which makes it then
# this still makes the value nil, except if type is
- # a XML node(where type['value'] is a Hash)
+ # an XML node(where type['value'] is a Hash)
value['type'] && !value['type'].is_a?(::Hash) && value.size == 1
end
@@ -241,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 dc86c92003..763d563231 100644
--- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -1,27 +1,38 @@
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 = { a: true, b: { c: [1, 2, 3] } }
+ # h2 = { a: false, b: { x: [3, 4, 5] } }
#
- # 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"]}
+ # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
+ #
+ # Like with Hash#merge in the standard library, a block can be provided
+ # to merge values:
+ #
+ # h1 = { a: 100, b: 200, c: { c1: 100 } }
+ # h2 = { b: 250, c: { c1: 200 } }
+ # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
+ # # => { a: 100, b: 450, c: { c1: 300 } }
def deep_merge(other_hash, &block)
dup.deep_merge!(other_hash, &block)
end
# Same as +deep_merge+, but modifies +self+.
def deep_merge!(other_hash, &block)
- other_hash.each_pair do |k,v|
- tv = self[k]
- if tv.is_a?(Hash) && v.is_a?(Hash)
- self[k] = tv.deep_merge(v, &block)
+ other_hash.each_pair do |current_key, other_value|
+ this_value = self[current_key]
+
+ self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
+ this_value.deep_merge(other_value, &block)
else
- self[k] = block && tv ? block.call(k, tv, v) : v
+ if block_given? && key?(current_key)
+ block.call(current_key, this_value, other_value)
+ else
+ other_value
+ end
end
end
+
self
end
end
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 970d6faa1d..28cb3e2a3b 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -18,6 +18,6 @@ class Hash
#
# b = { b: 1 }
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
- # # => {"b"=>32}
+ # # => {"b"=>1}
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 3d41aa8572..f4105f66b0 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -6,7 +6,8 @@ class Hash
# hash.transform_keys{ |key| key.to_s.upcase }
# # => {"NAME"=>"Rob", "AGE"=>"28"}
def transform_keys
- result = {}
+ return enum_for(:transform_keys) unless block_given?
+ result = self.class.new
each_key do |key|
result[yield(key)] = self[key]
end
@@ -16,6 +17,7 @@ class Hash
# Destructively convert all keys using the block operations.
# Same as transform_keys but modifies +self+.
def transform_keys!
+ return enum_for(:transform_keys!) unless block_given?
keys.each do |key|
self[yield(key)] = delete(key)
end
@@ -27,7 +29,7 @@ class Hash
# 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
@@ -44,7 +46,7 @@ class Hash
# 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
@@ -57,9 +59,11 @@ class Hash
end
alias_method :to_options!, :symbolize_keys!
- # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
- # 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.
+ # Validate all keys in a hash match <tt>*valid_keys</tt>, raising
+ # ArgumentError on a mismatch.
+ #
+ # Note that keys are treated differently than HashWithIndifferentAccess,
+ # meaning that string and symbol keys will not match.
#
# { 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'"
@@ -75,34 +79,26 @@ class Hash
# 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.
+ # nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_keys{ |key| key.to_s.upcase }
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
def deep_transform_keys(&block)
- result = {}
- each do |key, value|
- result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
- end
- result
+ _deep_transform_keys_in_object(self, &block)
end
# Destructively convert all keys by using the block operation.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_transform_keys!(&block)
- keys.each do |key|
- value = delete(key)
- self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
- end
- self
+ _deep_transform_keys_in_object!(self, &block)
end
# Returns a new hash with all keys converted to strings.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
@@ -114,14 +110,14 @@ class Hash
# Destructively convert all keys to strings.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_stringify_keys!
deep_transform_keys!{ |key| key.to_s }
end
# 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.
+ # and from all nested hashes and arrays.
#
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
#
@@ -133,8 +129,38 @@ class Hash
# Destructively convert all keys to symbols, as long as they respond
# to +to_sym+. This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_symbolize_keys!
deep_transform_keys!{ |key| key.to_sym rescue key }
end
+
+ private
+ # support methods for deep transforming nested hashes and arrays
+ def _deep_transform_keys_in_object(object, &block)
+ case object
+ when Hash
+ object.each_with_object({}) do |(key, value), result|
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
+ end
+ when Array
+ object.map {|e| _deep_transform_keys_in_object(e, &block) }
+ else
+ object
+ end
+ end
+
+ def _deep_transform_keys_in_object!(object, &block)
+ case object
+ when Hash
+ object.keys.each do |key|
+ value = object.delete(key)
+ object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
+ end
+ object
+ when Array
+ object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
+ else
+ object
+ end
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
new file mode 100644
index 0000000000..e9bcce761f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -0,0 +1,23 @@
+class Hash
+ # Returns a new hash with the results of running +block+ once for every value.
+ # The keys are unchanged.
+ #
+ # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
+ # # => { a: 2, b: 4, c: 6 }
+ def transform_values
+ return enum_for(:transform_values) unless block_given?
+ result = self.class.new
+ each do |key, value|
+ result[key] = yield(value)
+ end
+ result
+ end
+
+ # Destructive +transform_values+
+ def transform_values!
+ return enum_for(:transform_values!) unless block_given?
+ each do |key, value|
+ self[key] = yield(value)
+ end
+ end
+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 3b5e205244..80c531b694 100644
--- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
@@ -31,9 +31,13 @@ module Kernel
# For compatibility
def silence_stderr #:nodoc:
+ ActiveSupport::Deprecation.warn(
+ "#silence_stderr is deprecated and will be removed in the next release"
+ ) #not thread-safe
silence_stream(STDERR) { yield }
end
+ # Deprecated : this method is not thread safe
# Silences any stream for the duration of the block.
#
# silence_stream(STDOUT) do
@@ -41,6 +45,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')
@@ -80,6 +86,9 @@ module Kernel
# stream = capture(:stderr) { system('echo error 1>&2') }
# stream # => "error\n"
def capture(stream)
+ ActiveSupport::Deprecation.warn(
+ "#capture(stream) is deprecated and will be removed in the next release"
+ ) #not thread-safe
stream = stream.to_s
captured_stream = Tempfile.new(stream)
stream_io = eval("$#{stream}")
@@ -100,7 +109,12 @@ module Kernel
# Silences both STDOUT and STDERR, even for subprocesses.
#
# quietly { system 'bundle install' }
+ #
+ # This method is not thread-safe.
def quietly
+ ActiveSupport::Deprecation.warn(
+ "#quietly is deprecated and will be removed in the next release"
+ ) #not thread-safe
silence_stream(STDOUT) do
silence_stream(STDERR) do
yield
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/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index f855833a24..e926392952 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -170,38 +170,26 @@ class Module
# methods still accept two arguments.
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
- # The following generated methods call the target exactly once, storing
+ # The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
- if allow_nil
- 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}")
- 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
+ exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
+
+ method_def = [
+ "def #{method_prefix}#{method}(#{definition})",
+ " _ = #{to}",
+ " if !_.nil? || nil.respond_to?(:#{method})",
+ " _.#{method}(#{definition})",
+ " else",
+ " #{exception unless allow_nil}",
+ " end",
+ "end"
+ ].join ';'
module_eval(method_def, file, line)
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 704c4248d9..689fae4830 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -61,25 +61,7 @@ class Numeric
end
alias :fortnight :fortnights
- # 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
-
- # Reads best with argument: 10.minutes.until(time)
- alias :until :ago
-
- # 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 --
+ # 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
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index f4f9152d6a..f1106cca9b 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -2,6 +2,7 @@ require 'active_support/core_ext/object/acts_like'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/object/deep_dup'
+require 'active_support/core_ext/object/itself'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/inclusion'
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 9cd7485e2e..c5d59128e5 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -19,7 +19,7 @@
class Object
# Can you safely dup this object?
#
- # False for +nil+, +false+, +true+, symbol, and number objects;
+ # False for +nil+, +false+, +true+, symbol, number and BigDecimal(in 1.9.x) objects;
# true otherwise.
def duplicable?
true
@@ -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/itself.rb b/activesupport/lib/active_support/core_ext/object/itself.rb
new file mode 100644
index 0000000000..adedc20169
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/itself.rb
@@ -0,0 +1,12 @@
+class Object
+ unless respond_to?(:itself) # TODO: Remove this file when we drop support for Ruby < 2.2
+ # Returns the object itself. Useful when dealing with a chaining scenario, like Active Record scopes:
+ #
+ # Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at)
+ #
+ # @return Object
+ def itself
+ self
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
index 8e08cfbf26..698b2d1920 100644
--- a/activesupport/lib/active_support/core_ext/object/json.rb
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -26,7 +26,7 @@ require 'active_support/core_ext/module/aliasing'
# 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].each do |klass|
+[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)
@@ -162,7 +162,7 @@ end
class Time
def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
@@ -172,7 +172,7 @@ end
class Date
def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
strftime("%Y-%m-%d")
else
strftime("%Y/%m/%d")
@@ -182,7 +182,7 @@ end
class DateTime
def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
strftime('%Y/%m/%d %H:%M:%S %z')
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 3dcae6fc7f..0000000000
--- a/activesupport/lib/active_support/core_ext/object/to_json.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-ActiveSupport::Deprecation.warn 'You have required `active_support/core_ext/object/to_json`. ' \
- 'This file will be removed in Rails 4.2. You should require `active_support/core_ext/object/json` ' \
- 'instead.'
-
-require 'active_support/core_ext/object/json'
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 13be0038c2..684d4ef57e 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -1,62 +1 @@
-class Object
- # Alias of <tt>to_s</tt>.
- def to_param
- to_s
- end
-end
-
-class NilClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class TrueClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class FalseClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class Array
- # Calls <tt>to_param</tt> on all its elements and joins the result with
- # slashes. This is used by <tt>url_for</tt> in Action Pack.
- def to_param
- collect { |e| e.to_param }.join '/'
- end
-end
-
-class Hash
- # Returns a string representation of the receiver suitable for use as a URL
- # query string:
- #
- # {name: 'David', nationality: 'Danish'}.to_param
- # # => "name=David&nationality=Danish"
- #
- # An optional namespace can be passed to enclose the param names:
- #
- # {name: 'David', nationality: 'Danish'}.to_param('user')
- # # => "user[name]=David&user[nationality]=Danish"
- #
- # The string pairs "key=value" that conform the query string
- # are sorted lexicographically in ascending order.
- #
- # This method is also aliased as +to_query+.
- def to_param(namespace = nil)
- if empty?
- namespace ? nil.to_query(namespace) : ''
- else
- collect do |key, value|
- value.to_query(namespace ? "#{namespace}[#{key}]" : key)
- end.sort! * '&'
- end
- end
-end
+require 'active_support/core_ext/object/to_query'
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 37352fa608..ccd568bbf5 100644
--- a/activesupport/lib/active_support/core_ext/object/to_query.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_query.rb
@@ -1,17 +1,46 @@
-require 'active_support/core_ext/object/to_param'
+require 'cgi'
class Object
- # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the
- # param name.
- #
- # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work.
+ # Alias of <tt>to_s</tt>.
+ def to_param
+ to_s
+ end
+
+ # Converts an object into a string suitable for use as a URL query string,
+ # using the given <tt>key</tt> as the param name.
def to_query(key)
- require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
"#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
end
end
+class NilClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
+class TrueClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
+class FalseClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
class Array
+ # Calls <tt>to_param</tt> on all its elements and joins the result with
+ # slashes. This is used by <tt>url_for</tt> in Action Pack.
+ def to_param
+ collect { |e| e.to_param }.join '/'
+ end
+
# Converts an array into a string suitable for use as a URL query string,
# using the given +key+ as the param name.
#
@@ -28,5 +57,28 @@ class Array
end
class Hash
- alias_method :to_query, :to_param
+ # Returns a string representation of the receiver suitable for use as a URL
+ # query string:
+ #
+ # {name: 'David', nationality: 'Danish'}.to_query
+ # # => "name=David&nationality=Danish"
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # {name: 'David', nationality: 'Danish'}.to_query('user')
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
+ #
+ # The string pairs "key=value" that conform the query string
+ # are sorted lexicographically in ascending order.
+ #
+ # This method is also aliased as +to_param+.
+ def to_query(namespace = nil)
+ collect do |key, value|
+ unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
+ end
+ end.compact.sort! * '&'
+ end
+
+ alias_method :to_param, :to_query
end
diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb
index 42e388b065..42e87c4424 100644
--- a/activesupport/lib/active_support/core_ext/object/with_options.rb
+++ b/activesupport/lib/active_support/core_ext/object/with_options.rb
@@ -34,9 +34,22 @@ class Object
# body i18n.t :body, user_name: user.name
# end
#
+ # When you don't pass an explicit receiver, it executes the whole block
+ # in merging options context:
+ #
+ # class Account < ActiveRecord::Base
+ # with_options dependent: :destroy do
+ # has_many :customers
+ # has_many :products
+ # has_many :invoices
+ # has_many :expenses
+ # end
+ # end
+ #
# <tt>with_options</tt> can also be nested since the call is forwarded to its receiver.
# Each nesting level will merge inherited defaults in addition to their own.
- def with_options(options)
- yield ActiveSupport::OptionMerger.new(self, options)
+ def with_options(options, &block)
+ option_merger = ActiveSupport::OptionMerger.new(self, options)
+ block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
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 6018fd9641..ebd0dd3fc7 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -64,7 +64,7 @@ class String
# 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"
@@ -76,7 +76,7 @@ class String
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
to(limit - 1)
end
@@ -84,7 +84,7 @@ 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"
@@ -96,7 +96,7 @@ class String
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
from(-limit)
end
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index 49c0df6026..1dfaf76673 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -62,4 +62,28 @@ class String
"#{self[0, stop]}#{omission}"
end
+
+ # Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
+ #
+ # 'Once upon a time in a world far far away'.truncate_words(4)
+ # # => "Once upon a time..."
+ #
+ # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
+ #
+ # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
+ # # => "Once<br>upon<br>a<br>time<br>in..."
+ #
+ # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
+ #
+ # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
+ # # => "And they found that many... (continued)"
+ def truncate_words(words_count, options = {})
+ sep = options[:separator] || /\s+/
+ sep = Regexp.escape(sep.to_s) unless Regexp === sep
+ if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
+ $1 + (options[:omission] || '...')
+ else
+ dup
+ end
+ 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 cf9b1a4ec0..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
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 eb02b6a442..c761325108 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,13 @@
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', "\u2028" => '\u2028', "\u2029" => '\u2029' }
HTML_ESCAPE_REGEXP = /[&"'><]/
- HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
# A utility method for escaping HTML tag characters.
@@ -18,12 +19,7 @@ class ERB
# puts html_escape('is a > 0 & a < 10?')
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
- s = s.to_s
- if s.html_safe?
- s
- else
- s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe
- end
+ unwrapped_html_escape(s).html_safe
end
# Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
@@ -35,6 +31,18 @@ class ERB
singleton_class.send(:remove_method, :html_escape)
module_function :html_escape
+ # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
+ # This method is not for public consumption! Seriously!
+ def unwrapped_html_escape(s) # :nodoc:
+ s = s.to_s
+ if s.html_safe?
+ s
+ else
+ s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
+ end
+ end
+ module_function :unwrapped_html_escape
+
# A utility method for escaping HTML without affecting existing escaped entities.
#
# html_escape_once('1 < 2 &amp; 3')
@@ -124,7 +132,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
@@ -170,14 +178,19 @@ module ActiveSupport #:nodoc:
end
def concat(value)
- if !html_safe? || value.html_safe?
- super(value)
- else
- super(ERB::Util.h(value))
- end
+ super(html_escape_interpolated_argument(value))
end
alias << concat
+ def prepend(value)
+ super(html_escape_interpolated_argument(value))
+ end
+
+ def prepend!(value)
+ ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend
+ prepend value
+ end
+
def +(other)
dup.concat(other)
end
@@ -227,7 +240,8 @@ module ActiveSupport #:nodoc:
private
def html_escape_interpolated_argument(arg)
- (!html_safe? || arg.html_safe?) ? arg : ERB::Util.h(arg)
+ (!html_safe? || arg.html_safe?) ? arg :
+ arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE)
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb
index ac1ffa4128..4cd6634558 100644
--- a/activesupport/lib/active_support/core_ext/thread.rb
+++ b/activesupport/lib/active_support/core_ext/thread.rb
@@ -62,6 +62,13 @@ class Thread
_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
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index 9fd26156c7..dbf1f2f373 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -20,7 +20,7 @@ class Time
: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>.
#
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 59675d744e..93a11d4586 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -180,13 +180,14 @@ module ActiveSupport #:nodoc:
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.
+ # We assume that 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 therefore we assume that 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"
+ raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name
else
Object
end
@@ -515,9 +516,9 @@ module ActiveSupport #:nodoc:
end
end
- raise NameError,
- "uninitialized constant #{qualified_name}",
- caller.reject { |l| l.starts_with? __FILE__ }
+ name_error = NameError.new("uninitialized constant #{qualified_name}", const_name)
+ name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ })
+ raise name_error
end
# Remove the constants that have been autoloaded, and those that have been
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 7df4857c25..0ae641d05b 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
@@ -101,8 +105,7 @@ module ActiveSupport
# 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.
+ # Remove it when we drop support for 2.0.0-p353.
def ===(other) #:nodoc:
value === other
end
diff --git a/activesupport/lib/active_support/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb
deleted file mode 100644
index 81e63e76a7..0000000000
--- a/activesupport/lib/active_support/file_watcher.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveSupport
- class FileWatcher
- class Backend
- def initialize(path, watcher)
- @watcher = watcher
- @path = path
- end
-
- def trigger(files)
- @watcher.trigger(files)
- end
- end
-
- def initialize
- @regex_matchers = {}
- end
-
- def watch(pattern, &block)
- @regex_matchers[pattern] = block
- end
-
- def trigger(files)
- trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
-
- files.each do |file, state|
- @regex_matchers.each do |pattern, block|
- trigger_files[block][state] << file if pattern === file
- end
- end
-
- trigger_files.each do |block, payload|
- block.call payload
- end
- end
- end
-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 594a4ca938..3d8f2d572b 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -55,7 +55,7 @@ module ActiveSupport
end
def initialize(constructor = {})
- if constructor.is_a?(Hash)
+ if constructor.respond_to?(:to_hash)
super()
update(constructor)
else
@@ -72,8 +72,10 @@ 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
+ new_hash.default_proc = hash.default_proc if hash.default_proc
end
end
@@ -125,7 +127,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
@@ -175,7 +177,14 @@ module ActiveSupport
indices.collect { |key| self[convert_key(key)] }
end
- # Returns an exact copy of the hash.
+ # Returns a shallow copy of the hash.
+ #
+ # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } })
+ # dup = hash.dup
+ # dup[:a][:c] = 'c'
+ #
+ # hash[:a][:c] # => nil
+ # dup[:a][:c] # => "c"
def dup
self.class.new(self).tap do |new_hash|
new_hash.default = default
@@ -237,11 +246,11 @@ module ActiveSupport
# Convert to a regular hash with string keys.
def to_hash
- _new_hash= {}
+ _new_hash = Hash.new(default)
each do |key, value|
- _new_hash[convert_key(key)] = convert_value(value, for: :to_hash)
+ _new_hash[key] = convert_value(value, for: :to_hash)
end
- Hash.new(default).merge!(_new_hash)
+ _new_hash
end
protected
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 23cd6716e3..affcfb7398 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -8,8 +8,6 @@ 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.
@@ -36,7 +34,7 @@ module I18n
# 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?
+ enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
I18n.enforce_available_locales = false
app.config.i18n.each do |setting, value|
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index eda0edff28..97401ccec7 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -160,7 +160,7 @@ module ActiveSupport
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
- (@uncountables << words).flatten!
+ @uncountables += words.flatten.map(&:downcase)
end
# Specifies a humanized form of a string by a regular expression rule or
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index b642d87d76..18ba79a8f9 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -89,6 +89,7 @@ module ActiveSupport
#
# 'SSLError'.underscore.camelize # => "SslError"
def underscore(camel_cased_word)
+ 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')
@@ -98,26 +99,46 @@ module ActiveSupport
word
end
- # Capitalizes the first word, turns underscores into spaces, and strips a
- # trailing '_id' if present.
- # Like +titleize+, this is meant for creating pretty output.
+ # Tweaks an attribute name for display to end users.
+ #
+ # Specifically, +humanize+ performs these transformations:
+ #
+ # * Applies human inflection rules to the argument.
+ # * Deletes leading underscores, if any.
+ # * Removes a "_id" suffix if present.
+ # * Replaces underscores with spaces, if any.
+ # * Downcases all words except acronyms.
+ # * Capitalizes the first word.
#
# The capitalization of the first word can be turned off by setting the
- # optional parameter +capitalize+ to false.
- # By default, this parameter is true.
+ # +:capitalize+ option to false (default is true).
#
# humanize('employee_salary') # => "Employee salary"
# humanize('author_id') # => "Author"
# humanize('author_id', capitalize: false) # => "author"
+ # humanize('_id') # => "Id"
+ #
+ # If "SSL" was defined to be an acronym:
+ #
+ # humanize('ssl_error') # => "SSL error"
+ #
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.sub!(/\A_+/, '')
+ result.sub!(/_id\z/, '')
result.tr!('_', ' ')
- result.gsub!(/([a-z\d]*)/i) { |match|
+
+ result.gsub!(/([a-z\d]*)/i) do |match|
"#{inflections.acronyms[match] || match.downcase}"
- }
- result.gsub!(/^\w/) { |match| match.upcase } if options.fetch(:capitalize, true)
+ end
+
+ if options.fetch(:capitalize, true)
+ result.sub!(/\A\w/) { |match| match.upcase }
+ end
+
result
end
@@ -154,7 +175,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(/.*\./, '')))
@@ -171,6 +192,8 @@ module ActiveSupport
#
# 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ""
#
# See also +deconstantize+.
def demodulize(path)
@@ -227,7 +250,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.
@@ -241,8 +264,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)
@@ -325,10 +348,11 @@ module ActiveSupport
private
- # Mount a regular expression that will match part by part of the constant.
+ # Mounts a regular expression, returned as a string to ease interpolation,
+ # that will match part by part the given constant.
#
- # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/
- # const_regexp("::") # => /::/
+ # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
+ # const_regexp("::") # => "::"
def const_regexp(camel_cased_word) #:nodoc:
parts = camel_cased_word.split("::")
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index ea3cdcd024..62caff77a3 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -213,7 +213,8 @@ module ActiveSupport
end
# Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1.
- if '<3'.respond_to?(:scrub)
+ # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
+ if '<3'.respond_to?(:scrub) && !defined?(Rubinius)
# Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
# resulting in a valid UTF-8 string.
#
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index 7a96c66626..325a3d75dc 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -141,6 +141,11 @@ module ActiveSupport
#
# ActiveSupport::Notifications.unsubscribe(subscriber)
#
+ # You can also unsubscribe by passing the name of the subscriber object. Note
+ # that this will unsubscribe all subscriptions with the given name:
+ #
+ # ActiveSupport::Notifications.unsubscribe("render")
+ #
# == Default Queue
#
# Notifications ships with a queue implementation that consumes and publishes events
@@ -173,8 +178,8 @@ module ActiveSupport
unsubscribe(subscriber)
end
- def unsubscribe(args)
- notifier.unsubscribe(args)
+ def unsubscribe(subscriber_or_name)
+ notifier.unsubscribe(subscriber_or_name)
end
def instrumenter
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 8f5fa646e8..6bf8c7d5de 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -25,9 +25,15 @@ module ActiveSupport
subscriber
end
- def unsubscribe(subscriber)
+ def unsubscribe(subscriber_or_name)
synchronize do
- @subscribers.reject! { |s| s.matches?(subscriber) }
+ case subscriber_or_name
+ when String
+ @subscribers.reject! { |s| s.matches?(subscriber_or_name) }
+ else
+ @subscribers.delete(subscriber_or_name)
+ end
+
@listeners_for.clear
end
end
@@ -97,12 +103,11 @@ module ActiveSupport
end
def subscribed_to?(name)
- @pattern === name.to_s
+ @pattern === name
end
- def matches?(subscriber_or_name)
- self === subscriber_or_name ||
- @pattern && @pattern === subscriber_or_name
+ def matches?(name)
+ @pattern && @pattern === name
end
end
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index b169e3af01..5ecda9593a 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -232,12 +232,8 @@ module ActiveSupport
# number_to_human_size(1234567, precision: 2) # => 1.2 MB
# number_to_human_size(483989, precision: 2) # => 470 KB
# number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
- #
- # Non-significant zeros after the fractional separator are stripped out by
- # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that):
- #
- # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
- # number_to_human_size(524288000, precision: 5) # => "500 MB"
+ # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
+ # number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
NumberToHumanSizeConverter.convert(number, options)
end
@@ -305,12 +301,15 @@ module ActiveSupport
# separator: ',',
# significant: false) # => "1,2 Million"
#
+ # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12345012345, significant: false) # => "12.345 Billion"
+ #
# Non-significant zeros after the decimal separator are stripped
# out by default (set <tt>:strip_insignificant_zeros</tt> to
# +false+ to change that):
#
- # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion"
- # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12.00001) # => "12"
+ # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
#
# ==== Custom Unit Quantifiers
#
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
index 6405afc9a6..d85cc086d7 100644
--- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -13,7 +13,9 @@ module ActiveSupport
def parts
left, right = number.to_s.split('.')
- left.gsub!(DELIMITED_REGEX) { "#{$1}#{options[:delimiter]}" }
+ left.gsub!(DELIMITED_REGEX) do |digit_to_delimit|
+ "#{digit_to_delimit}#{options[:delimiter]}"
+ end
[left, right].compact
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
index c42354fc83..01597b288a 100644
--- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -12,11 +12,7 @@ module ActiveSupport
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
+ @number = BigDecimal(number, digit_count(number.to_i) + precision)
else
@number = number.to_d
end
@@ -32,8 +28,7 @@ module ActiveSupport
end
formatted_string =
- case rounded_number
- when BigDecimal
+ if BigDecimal === rounded_number && rounded_number.finite?
s = rounded_number.to_s('F') + '0'*precision
a, b = s.split('.', 2)
a + '.' + b[0, precision]
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 58a2ce2105..4680d5acb7 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -28,6 +28,10 @@ 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
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index 4b9b48539f..98be78b41b 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -64,12 +64,21 @@ module ActiveSupport
def add_event_subscriber(event)
return if %w{ start finish }.include?(event.to_s)
- notifier.subscribe("#{event}.#{namespace}", subscriber)
+ pattern = "#{event}.#{namespace}"
+
+ # don't add multiple subscribers (eg. if methods are redefined)
+ return if subscriber.patterns.include?(pattern)
+
+ subscriber.patterns << pattern
+ notifier.subscribe(pattern, subscriber)
end
end
+ attr_reader :patterns # :nodoc:
+
def initialize
@queue_key = [self.class.name, object_id].join "-"
+ @patterns = []
super
end
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index 2fb5c04316..0df599b692 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -11,25 +11,15 @@ require 'active_support/testing/time_helpers'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/deprecation'
-begin
- silence_warnings { require 'mocha/setup' }
-rescue LoadError
-end
-
module ActiveSupport
class TestCase < ::Minitest::Test
Assertion = Minitest::Assertion
- alias_method :method_name, :name
-
- $tags = {}
- def self.for_tag(tag)
- yield if $tags[tag]
+ class << self
+ alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent!
end
- # FIXME: we have tests that depend on run order, we should fix that and
- # remove this method call.
- self.i_suck_and_my_tests_are_order_dependent!
+ alias_method :method_name, :name
include ActiveSupport::Testing::TaggedLogging
include ActiveSupport::Testing::SetupAndTeardown
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index 76a591bc3b..11cca82995 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -9,7 +9,7 @@ module ActiveSupport
#
# assert_not nil # => true
# assert_not false # => true
- # assert_not 'foo' # => 'foo' is not nil or false
+ # assert_not 'foo' # => Expected "foo" to be nil or false
#
# An error message can be specified.
#
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
index c349bb5fb1..0bf3643a56 100644
--- a/activesupport/lib/active_support/testing/declarative.rb
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -1,30 +1,6 @@
module ActiveSupport
module Testing
module Declarative
-
- def self.extended(klass) #:nodoc:
- klass.class_eval do
-
- unless method_defined?(:describe)
- def self.describe(text)
- 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
-
- end
- end
-
unless defined?(Spec)
# Helper to define a test method using a String. Under the hood, it replaces
# spaces with underscores and defines the test method.
@@ -34,7 +10,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/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 908af176be..68bda35980 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -70,14 +70,24 @@ module ActiveSupport
exit!
else
Tempfile.open("isolation") do |tmpfile|
- ENV["ISOLATION_TEST"] = self.class.name
- ENV["ISOLATION_OUTPUT"] = tmpfile.path
+ env = {
+ ISOLATION_TEST: self.class.name,
+ ISOLATION_OUTPUT: tmpfile.path
+ }
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}`
-
- ENV.delete("ISOLATION_TEST")
- ENV.delete("ISOLATION_OUTPUT")
+ orig_args = ORIG_ARGV.join(" ")
+ test_opts = "-n#{self.class.name}##{self.name}"
+ command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}"
+
+ # IO.popen lets us pass env in a cross-platform way
+ child = IO.popen([env, command])
+
+ begin
+ Process.wait(child.pid)
+ rescue Errno::ECHILD # The child process may exit before we wait
+ nil
+ end
return tmpfile.read.unpack("m")[0]
end
diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb
index f4cee64091..843ce4a867 100644
--- a/activesupport/lib/active_support/testing/tagged_logging.rb
+++ b/activesupport/lib/active_support/testing/tagged_logging.rb
@@ -6,7 +6,7 @@ module ActiveSupport
attr_writer :tagged_logger
def before_setup
- if tagged_logger
+ if tagged_logger && tagged_logger.info?
heading = "#{self.class}: #{name}"
divider = '-' * heading.size
tagged_logger.info divider
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
index 9e0a3d6345..eefa84262e 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -10,7 +10,7 @@ module ActiveSupport
def stub_object(object, method_name, return_value)
key = [object.object_id, method_name]
- if (stub = @stubs[key])
+ if stub = @stubs[key]
unstub_object(stub)
end
@@ -61,14 +61,23 @@ module ActiveSupport
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.
+ # 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:
#
@@ -78,12 +87,21 @@ module ActiveSupport
# end
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
def travel_to(date_or_time, &block)
- simple_stubs.stub_object(Time, :now, date_or_time.to_time)
- simple_stubs.stub_object(Date, :today, date_or_time.to_date)
+ 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?
- block.call
- travel_back
+ begin
+ block.call
+ ensure
+ travel_back
+ end
end
end
diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb
index 92a593965e..ea2d3391bd 100644
--- a/activesupport/lib/active_support/time.rb
+++ b/activesupport/lib/active_support/time.rb
@@ -1,5 +1,3 @@
-require 'active_support'
-
module ActiveSupport
autoload :Duration, 'active_support/duration'
autoload :TimeWithZone, 'active_support/time_with_zone'
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index c25c97cfa8..4a0ed356b1 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -185,8 +185,11 @@ module ActiveSupport
end
alias_method :rfc822, :rfc2822
- # <tt>:db</tt> format outputs time in UTC; all others output time in local.
- # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly.
+ # Returns a string of the object's date and time.
+ # Accepts an optional <tt>format</tt>:
+ # * <tt>:default</tt> - default value, mimics Ruby 1.9 Time#to_s format.
+ # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db).
+ # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb.
def to_s(format = :default)
if format == :db
utc.to_s(format)
@@ -259,7 +262,7 @@ module ActiveSupport
# If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time,
# otherwise move backwards #utc, for accuracy when moving across DST boundaries
if other.acts_like?(:time)
- utc.to_f - other.to_f
+ to_time - other.to_time
elsif duration_of_variable_length?(other)
method_missing(:-, other)
else
@@ -350,6 +353,14 @@ module ActiveSupport
initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc)
end
+ # respond_to_missing? is not called in some cases, such as when type conversion is
+ # performed with Kernel#String
+ def respond_to?(sym, include_priv = false)
+ # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow
+ return false if sym.to_sym == :to_str
+ super
+ end
+
# Ensure proxy class responds to all methods that underlying time instance
# responds to.
def respond_to_missing?(sym, include_priv)
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index eb785d46ce..ee62523824 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -188,16 +188,72 @@ module ActiveSupport
@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.
- #
- # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
- def self.seconds_to_utc_offset(seconds, colon = true)
- format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
- sign = (seconds < 0 ? '-' : '+')
- hours = seconds.abs / 3600
- minutes = (seconds.abs % 3600) / 60
- format % [sign, hours, minutes]
+ class << self
+ # Assumes self represents an offset from UTC in seconds (as returned from
+ # Time#utc_offset) and turns this into an +HH:MM formatted string.
+ #
+ # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
+ def seconds_to_utc_offset(seconds, colon = true)
+ format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
+ sign = (seconds < 0 ? '-' : '+')
+ hours = seconds.abs / 3600
+ minutes = (seconds.abs % 3600) / 60
+ format % [sign, hours, minutes]
+ end
+
+ def find_tzinfo(name)
+ TZInfo::TimezoneProxy.new(MAPPING[name] || name)
+ end
+
+ alias_method :create, :new
+
+ # 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
+
+ # 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
+ @zones ||= zones_map.values.sort
+ end
+
+ def zones_map
+ @zones_map ||= begin
+ MAPPING.each_key {|place| self[place]} # load all the zones
+ @lazy_zones_map
+ end
+ end
+
+ # Locate a specific time zone object. If the argument is a string, it
+ # is interpreted to mean the name of the timezone to locate. If it is a
+ # numeric value it is either the hour offset, or the second offset, of the
+ # timezone to find. (The first one with that offset will be returned.)
+ # Returns +nil+ if no such time zone is known to the system.
+ def [](arg)
+ case arg
+ when String
+ begin
+ @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset }
+ rescue TZInfo::InvalidTimezoneIdentifier
+ nil
+ end
+ when Numeric, ActiveSupport::Duration
+ arg *= 3600 if arg.abs <= 13
+ all.find { |z| z.utc_offset == arg.to_i }
+ else
+ raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
+ end
+ end
+
+ # A convenience method for returning a collection of TimeZone objects
+ # for time zones in the USA.
+ def us_zones
+ @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
+ end
end
include Comparable
@@ -282,14 +338,19 @@ 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)
+ #
+ # However, if the date component is not provided, but any other upper
+ # components are supplied, then the day of the month defaults to 1:
+ #
+ # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
+ def parse(str, now=now())
parts = Date._parse(str, false)
return if parts.empty?
time = Time.new(
parts.fetch(:year, now.year),
parts.fetch(:mon, now.month),
- parts.fetch(:mday, now.day),
+ parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
@@ -356,66 +417,9 @@ module ActiveSupport
tzinfo.periods_for_local(time)
end
- def self.find_tzinfo(name)
- TZInfo::TimezoneProxy.new(MAPPING[name] || name)
- end
-
- class << self
- alias_method :create, :new
-
- # 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
-
- # 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
- @zones ||= zones_map.values.sort
- end
-
- def zones_map
- @zones_map ||= begin
- MAPPING.each_key {|place| self[place]} # load all the zones
- @lazy_zones_map
- end
- end
-
- # Locate a specific time zone object. If the argument is a string, it
- # is interpreted to mean the name of the timezone to locate. If it is a
- # numeric value it is either the hour offset, or the second offset, of the
- # timezone to find. (The first one with that offset will be returned.)
- # Returns +nil+ if no such time zone is known to the system.
- def [](arg)
- case arg
- when String
- begin
- @lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset }
- rescue TZInfo::InvalidTimezoneIdentifier
- nil
- end
- when Numeric, ActiveSupport::Duration
- arg *= 3600 if arg.abs <= 13
- all.find { |z| z.utc_offset == arg.to_i }
- else
- raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
- end
- end
-
- # A convenience method for returning a collection of TimeZone objects
- # for time zones in the USA.
- def us_zones
- @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
- end
- end
-
private
-
- def time_now
- Time.now
- end
+ def time_now
+ Time.now
+ end
end
end
diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb
index b3f0e7198d..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.beta1"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments
- STRING = ActiveSupport.version.to_s
+ gem_version
end
end
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index 0b393e0c7a..7ffcae6007 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -36,3 +36,5 @@ end
def jruby_skip(message = '')
skip message if defined?(JRUBY_VERSION)
end
+
+require 'mocha/setup' # FIXME: stop using mocha
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index c3c65cf805..8287e62f4c 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -60,36 +60,25 @@ class CacheKeyTest < ActiveSupport::TestCase
end
def test_expand_cache_key_with_rails_cache_id
- begin
- ENV['RAILS_CACHE_ID'] = 'c99'
+ with_env('RAILS_CACHE_ID' => 'c99') do
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo])
assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar])
assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm)
assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm)
assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
- ensure
- ENV['RAILS_CACHE_ID'] = nil
end
end
def test_expand_cache_key_with_rails_app_version
- begin
- ENV['RAILS_APP_VERSION'] = 'rails3'
+ with_env('RAILS_APP_VERSION' => 'rails3') do
assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- ensure
- ENV['RAILS_APP_VERSION'] = nil
end
end
def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
- begin
- ENV['RAILS_CACHE_ID'] = 'c99'
- ENV['RAILS_APP_VERSION'] = 'rails3'
+ with_env('RAILS_CACHE_ID' => 'c99', 'RAILS_APP_VERSION' => 'rails3') do
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- ensure
- ENV['RAILS_CACHE_ID'] = nil
- ENV['RAILS_APP_VERSION'] = nil
end
end
@@ -124,6 +113,16 @@ class CacheKeyTest < ActiveSupport::TestCase
def test_expand_cache_key_of_array_like_object
assert_equal 'foo/bar/baz', ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
end
+
+ private
+
+ def with_env(kv)
+ old_values = {}
+ kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
+ yield
+ ensure
+ old_values.each { |key, value| ENV[key] = value}
+ end
end
class CacheStoreSettingTest < ActiveSupport::TestCase
@@ -297,20 +296,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 }
- values = @cache.fetch_multi(foo, bar) {|object| object.title }
- assert_equal(["FOO!", "BAM!"], values)
+ assert_equal({ foo => 'FOO!', bar => 'BAM!' }, values)
end
def test_read_and_write_compressed_small_data
@@ -691,6 +691,11 @@ class FileStoreTest < ActiveSupport::TestCase
assert File.exist?(filepath)
end
+ def test_long_keys
+ @cache.write("a"*10000, 1)
+ assert_equal 1, @cache.read("a"*10000)
+ end
+
def test_key_transformation
key = @cache.send(:key_file_path, "views/index?id=1")
assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb
index dd67a45cf6..05580352a9 100644
--- a/activesupport/test/clean_backtrace_test.rb
+++ b/activesupport/test/clean_backtrace_test.rb
@@ -34,6 +34,11 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase
[ "/other/class.rb" ],
@bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb" ])
end
+
+ test "backtrace cleaner should allow removing silencer" do
+ @bc.remove_silencers!
+ assert_equal ["/mongrel/stuff.rb"], @bc.clean(["/mongrel/stuff.rb"])
+ end
end
class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index a74ee880b2..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
@@ -73,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/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index bbeb710a0c..8a9fd4996b 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -27,13 +27,24 @@ module ConstantizeTestCases
assert_equal Ace::Base::Case, yield("Ace::Base::Case")
assert_equal Ace::Base::Case, yield("::Ace::Base::Case")
assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice")
+ assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Fase::Dice")
assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice")
+
assert_equal Ace::Gas::Case, yield("Ace::Gas::Case")
assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice")
+ assert_equal Ace::Base::Case::Dice, yield("Ace::Gas::Case::Dice")
+
assert_equal Case::Dice, yield("Case::Dice")
+ assert_equal AddtlGlobalConstants::Case::Dice, yield("Case::Dice")
+ assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice")
+
assert_equal Case::Dice, yield("Object::Case::Dice")
+ assert_equal AddtlGlobalConstants::Case::Dice, yield("Object::Case::Dice")
+ assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice")
+
assert_equal ConstantizeTestCases, yield("ConstantizeTestCases")
assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases")
+
assert_raises(NameError) { yield("UnknownClass") }
assert_raises(NameError) { yield("UnknownClass::Ace") }
assert_raises(NameError) { yield("UnknownClass::Ace::Base") }
diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb
new file mode 100644
index 0000000000..f14f64421d
--- /dev/null
+++ b/activesupport/test/core_ext/array/access_test.rb
@@ -0,0 +1,30 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class AccessTest < ActiveSupport::TestCase
+ def test_from
+ assert_equal %w( a b c d ), %w( a b c d ).from(0)
+ assert_equal %w( c d ), %w( a b c d ).from(2)
+ assert_equal %w(), %w( a b c d ).from(10)
+ assert_equal %w( d e ), %w( a b c d e ).from(-2)
+ assert_equal %w(), %w( a b c d e ).from(-10)
+ end
+
+ def test_to
+ assert_equal %w( a ), %w( a b c d ).to(0)
+ assert_equal %w( a b c ), %w( a b c d ).to(2)
+ assert_equal %w( a b c d ), %w( a b c d ).to(10)
+ assert_equal %w( a b c ), %w( a b c d ).to(-2)
+ assert_equal %w(), %w( a b c ).to(-10)
+ end
+
+ def test_specific_accessor
+ array = (1..42).to_a
+
+ assert_equal array[1], array.second
+ assert_equal array[2], array.third
+ assert_equal array[3], array.fourth
+ assert_equal array[4], array.fifth
+ assert_equal array[41], array.forty_two
+ end
+end
diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb
new file mode 100644
index 0000000000..577b889410
--- /dev/null
+++ b/activesupport/test/core_ext/array/conversions_test.rb
@@ -0,0 +1,197 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/big_decimal'
+require 'active_support/core_ext/hash'
+require 'active_support/core_ext/string'
+
+class ToSentenceTest < ActiveSupport::TestCase
+ def test_plain_array_to_sentence
+ assert_equal "", [].to_sentence
+ assert_equal "one", ['one'].to_sentence
+ assert_equal "one and two", ['one', 'two'].to_sentence
+ assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence
+ end
+
+ def test_to_sentence_with_words_connector
+ assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' ')
+ assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' & ')
+ assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(words_connector: nil)
+ end
+
+ def test_to_sentence_with_last_word_connector
+ assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(last_word_connector: ', and also ')
+ assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(last_word_connector: nil)
+ assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' ')
+ assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' and ')
+ end
+
+ def test_two_elements
+ assert_equal "one and two", ['one', 'two'].to_sentence
+ assert_equal "one two", ['one', 'two'].to_sentence(two_words_connector: ' ')
+ end
+
+ def test_one_element
+ assert_equal "one", ['one'].to_sentence
+ end
+
+ def test_one_element_not_same_object
+ elements = ["one"]
+ assert_not_equal elements[0].object_id, elements.to_sentence.object_id
+ end
+
+ def test_one_non_string_element
+ assert_equal '1', [1].to_sentence
+ end
+
+ def test_does_not_modify_given_hash
+ options = { words_connector: ' ' }
+ assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options)
+ assert_equal({ words_connector: ' ' }, options)
+ end
+
+ def test_with_blank_elements
+ assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence
+ end
+
+ def test_with_invalid_options
+ exception = assert_raise ArgumentError do
+ ['one', 'two'].to_sentence(passing: 'invalid option')
+ end
+
+ assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale"
+ end
+end
+
+class ToSTest < ActiveSupport::TestCase
+ class TestDB
+ @@counter = 0
+ def id
+ @@counter += 1
+ end
+ end
+
+ def test_to_s_db
+ collection = [TestDB.new, TestDB.new, TestDB.new]
+
+ assert_equal "null", [].to_s(:db)
+ assert_equal "1,2,3", collection.to_s(:db)
+ end
+end
+
+class ToXmlTest < ActiveSupport::TestCase
+ def test_to_xml_with_hash_elements
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 },
+ { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ ].to_xml(skip_instruct: true, indent: 0)
+
+ assert_equal '<objects type="array"><object>', xml.first(30)
+ assert xml.include?(%(<age type="integer">26</age>)), xml
+ assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml
+ assert xml.include?(%(<name>David</name>)), xml
+ assert xml.include?(%(<age type="integer">31</age>)), xml
+ assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml
+ assert xml.include?(%(<name>Jason</name>)), xml
+ end
+
+ def test_to_xml_with_non_hash_elements
+ xml = [1, 2, 3].to_xml(skip_instruct: true, indent: 0)
+
+ assert_equal '<fixnums type="array"><fixnum', xml.first(29)
+ assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml
+ end
+
+ def test_to_xml_with_non_hash_different_type_elements
+ xml = [1, 2.0, '3'].to_xml(skip_instruct: true, indent: 0)
+
+ assert_equal '<objects type="array"><object', xml.first(29)
+ assert xml.include?(%(<object type="integer">1</object>)), xml
+ assert xml.include?(%(<object type="float">2.0</object>)), xml
+ assert xml.include?(%(object>3</object>)), xml
+ end
+
+ def test_to_xml_with_dedicated_name
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 }, { name: "Jason", age: 31 }
+ ].to_xml(skip_instruct: true, indent: 0, root: "people")
+
+ assert_equal '<people type="array"><person>', xml.first(29)
+ end
+
+ def test_to_xml_with_options
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 0)
+
+ assert_equal "<objects><object>", xml.first(17)
+ assert xml.include?(%(<street-address>Paulina</street-address>))
+ assert xml.include?(%(<name>David</name>))
+ assert xml.include?(%(<street-address>Evergreen</street-address>))
+ assert xml.include?(%(<name>Jason</name>))
+ end
+
+ def test_to_xml_with_indent_set
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 4)
+
+ assert_equal "<objects>\n <object>", xml.first(22)
+ assert xml.include?(%(\n <street-address>Paulina</street-address>))
+ assert xml.include?(%(\n <name>David</name>))
+ assert xml.include?(%(\n <street-address>Evergreen</street-address>))
+ assert xml.include?(%(\n <name>Jason</name>))
+ end
+
+ def test_to_xml_with_dasherize_false
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: false)
+
+ assert_equal "<objects><object>", xml.first(17)
+ assert xml.include?(%(<street_address>Paulina</street_address>))
+ assert xml.include?(%(<street_address>Evergreen</street_address>))
+ end
+
+ def test_to_xml_with_dasherize_true
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: true)
+
+ assert_equal "<objects><object>", xml.first(17)
+ assert xml.include?(%(<street-address>Paulina</street-address>))
+ assert xml.include?(%(<street-address>Evergreen</street-address>))
+ end
+
+ def test_to_xml_with_instruct
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 },
+ { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ ].to_xml(skip_instruct: false, indent: 0)
+
+ assert_match(/^<\?xml [^>]*/, xml)
+ assert_equal 0, xml.rindex(/<\?xml /)
+ end
+
+ def test_to_xml_with_block
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 },
+ { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ ].to_xml(skip_instruct: true, indent: 0) do |builder|
+ builder.count 2
+ end
+
+ assert xml.include?(%(<count>2</count>)), xml
+ end
+
+ def test_to_xml_with_empty
+ xml = [].to_xml
+ assert_match(/type="array"\/>/, xml)
+ end
+
+ def test_to_xml_dups_options
+ options = { skip_instruct: true }
+ [].to_xml(options)
+ # :builder, etc, shouldn't be added to options
+ assert_equal({ skip_instruct: true }, options)
+ end
+end
diff --git a/activesupport/test/core_ext/array/extract_options_test.rb b/activesupport/test/core_ext/array/extract_options_test.rb
new file mode 100644
index 0000000000..0481a507cf
--- /dev/null
+++ b/activesupport/test/core_ext/array/extract_options_test.rb
@@ -0,0 +1,45 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/hash'
+
+class ExtractOptionsTest < ActiveSupport::TestCase
+ class HashSubclass < Hash
+ end
+
+ class ExtractableHashSubclass < Hash
+ def extractable_options?
+ true
+ end
+ end
+
+ def test_extract_options
+ assert_equal({}, [].extract_options!)
+ assert_equal({}, [1].extract_options!)
+ assert_equal({ a: :b }, [{ a: :b }].extract_options!)
+ assert_equal({ a: :b }, [1, { a: :b }].extract_options!)
+ end
+
+ def test_extract_options_doesnt_extract_hash_subclasses
+ hash = HashSubclass.new
+ hash[:foo] = 1
+ array = [hash]
+ options = array.extract_options!
+ assert_equal({}, options)
+ assert_equal([hash], array)
+ end
+
+ def test_extract_options_extracts_extractable_subclass
+ hash = ExtractableHashSubclass.new
+ hash[:foo] = 1
+ array = [hash]
+ options = array.extract_options!
+ assert_equal({ foo: 1 }, options)
+ assert_equal([], array)
+ end
+
+ def test_extract_options_extracts_hash_with_indifferent_access
+ array = [{ foo: 1 }.with_indifferent_access]
+ options = array.extract_options!
+ assert_equal(1, options[:foo])
+ end
+end
diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb
new file mode 100644
index 0000000000..2eb0f05141
--- /dev/null
+++ b/activesupport/test/core_ext/array/grouping_test.rb
@@ -0,0 +1,126 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class GroupingTest < ActiveSupport::TestCase
+ def setup
+ Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
+ end
+
+ def teardown
+ Fixnum.send :public, :/
+ end
+
+ def test_in_groups_of_with_perfect_fit
+ groups = []
+ ('a'..'i').to_a.in_groups_of(3) do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups
+ assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3)
+ end
+
+ def test_in_groups_of_with_padding
+ groups = []
+ ('a'..'g').to_a.in_groups_of(3) do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups
+ end
+
+ def test_in_groups_of_pads_with_specified_values
+ groups = []
+
+ ('a'..'g').to_a.in_groups_of(3, 'foo') do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), %w(g foo foo)], groups
+ end
+
+ def test_in_groups_of_without_padding
+ groups = []
+
+ ('a'..'g').to_a.in_groups_of(3, false) do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), %w(g)], groups
+ end
+
+ def test_in_groups_returned_array_size
+ array = (1..7).to_a
+
+ 1.upto(array.size + 1) do |number|
+ assert_equal number, array.in_groups(number).size
+ end
+ end
+
+ def test_in_groups_with_empty_array
+ assert_equal [[], [], []], [].in_groups(3)
+ end
+
+ def test_in_groups_with_block
+ array = (1..9).to_a
+ groups = []
+
+ array.in_groups(3) do |group|
+ groups << group
+ end
+
+ assert_equal array.in_groups(3), groups
+ end
+
+ def test_in_groups_with_perfect_fit
+ assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
+ (1..9).to_a.in_groups(3)
+ end
+
+ def test_in_groups_with_padding
+ array = (1..7).to_a
+
+ assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]],
+ array.in_groups(3)
+ assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']],
+ array.in_groups(3, 'foo')
+ end
+
+ def test_in_groups_without_padding
+ assert_equal [[1, 2, 3], [4, 5], [6, 7]],
+ (1..7).to_a.in_groups(3, false)
+ end
+
+ def test_in_groups_invalid_argument
+ assert_raises(ArgumentError) { [].in_groups_of(0) }
+ assert_raises(ArgumentError) { [].in_groups_of(-1) }
+ assert_raises(ArgumentError) { [].in_groups_of(nil) }
+ end
+end
+
+class SplitTest < ActiveSupport::TestCase
+ def test_split_with_empty_array
+ assert_equal [[]], [].split(0)
+ end
+
+ def test_split_with_argument
+ 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
+ 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
+ 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
diff --git a/activesupport/test/core_ext/array/prepend_append_test.rb b/activesupport/test/core_ext/array/prepend_append_test.rb
new file mode 100644
index 0000000000..762aa69b2b
--- /dev/null
+++ b/activesupport/test/core_ext/array/prepend_append_test.rb
@@ -0,0 +1,12 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class PrependAppendTest < ActiveSupport::TestCase
+ def test_append
+ assert_equal [1, 2], [1].append(2)
+ end
+
+ def test_prepend
+ assert_equal [2, 1], [1].prepend(2)
+ end
+end
diff --git a/activesupport/test/core_ext/array/wrap_test.rb b/activesupport/test/core_ext/array/wrap_test.rb
new file mode 100644
index 0000000000..baf426506f
--- /dev/null
+++ b/activesupport/test/core_ext/array/wrap_test.rb
@@ -0,0 +1,77 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class WrapTest < ActiveSupport::TestCase
+ class FakeCollection
+ def to_ary
+ ["foo", "bar"]
+ end
+ end
+
+ class Proxy
+ def initialize(target) @target = target end
+ def method_missing(*a) @target.send(*a) end
+ end
+
+ class DoubtfulToAry
+ def to_ary
+ :not_an_array
+ end
+ end
+
+ class NilToAry
+ def to_ary
+ nil
+ end
+ end
+
+ def test_array
+ ary = %w(foo bar)
+ assert_same ary, Array.wrap(ary)
+ end
+
+ def test_nil
+ assert_equal [], Array.wrap(nil)
+ end
+
+ def test_object
+ o = Object.new
+ assert_equal [o], Array.wrap(o)
+ end
+
+ def test_string
+ assert_equal ["foo"], Array.wrap("foo")
+ end
+
+ def test_string_with_newline
+ assert_equal ["foo\nbar"], Array.wrap("foo\nbar")
+ end
+
+ def test_object_with_to_ary
+ assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new)
+ end
+
+ def test_proxy_object
+ p = Proxy.new(Object.new)
+ assert_equal [p], Array.wrap(p)
+ end
+
+ def test_proxy_to_object_with_to_ary
+ p = Proxy.new(FakeCollection.new)
+ assert_equal [p], Array.wrap(p)
+ end
+
+ def test_struct
+ o = Struct.new(:foo).new(123)
+ assert_equal [o], Array.wrap(o)
+ end
+
+ def test_wrap_returns_wrapped_if_to_ary_returns_nil
+ o = NilToAry.new
+ assert_equal [o], Array.wrap(o)
+ end
+
+ def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array
+ assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new)
+ end
+end
diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb
deleted file mode 100644
index 57722fd52a..0000000000
--- a/activesupport/test/core_ext/array_ext_test.rb
+++ /dev/null
@@ -1,451 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/big_decimal'
-require 'active_support/core_ext/object/conversions'
-
-require 'active_support/core_ext' # FIXME: pulling in all to_xml extensions
-require 'active_support/hash_with_indifferent_access'
-
-class ArrayExtAccessTests < ActiveSupport::TestCase
- def test_from
- assert_equal %w( a b c d ), %w( a b c d ).from(0)
- assert_equal %w( c d ), %w( a b c d ).from(2)
- assert_equal %w(), %w( a b c d ).from(10)
- end
-
- def test_to
- assert_equal %w( a ), %w( a b c d ).to(0)
- assert_equal %w( a b c ), %w( a b c d ).to(2)
- assert_equal %w( a b c d ), %w( a b c d ).to(10)
- end
-
- def test_second_through_tenth
- array = (1..42).to_a
-
- assert_equal array[1], array.second
- assert_equal array[2], array.third
- assert_equal array[3], array.fourth
- assert_equal array[4], array.fifth
- assert_equal array[41], array.forty_two
- end
-end
-
-class ArrayExtToParamTests < ActiveSupport::TestCase
- class ToParam < String
- def to_param
- "#{self}1"
- end
- end
-
- def test_string_array
- assert_equal '', %w().to_param
- assert_equal 'hello/world', %w(hello world).to_param
- assert_equal 'hello/10', %w(hello 10).to_param
- end
-
- def test_number_array
- assert_equal '10/20', [10, 20].to_param
- end
-
- def test_to_param_array
- assert_equal 'custom1/param1', [ToParam.new('custom'), ToParam.new('param')].to_param
- end
-end
-
-class ArrayExtToSentenceTests < ActiveSupport::TestCase
- def test_plain_array_to_sentence
- assert_equal "", [].to_sentence
- assert_equal "one", ['one'].to_sentence
- assert_equal "one and two", ['one', 'two'].to_sentence
- assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence
- end
-
- def test_to_sentence_with_words_connector
- assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' ')
- assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' & ')
- assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(:words_connector => nil)
- end
-
- def test_to_sentence_with_last_word_connector
- assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ', and also ')
- assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(:last_word_connector => nil)
- assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' ')
- assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' and ')
- end
-
- def test_two_elements
- assert_equal "one and two", ['one', 'two'].to_sentence
- assert_equal "one two", ['one', 'two'].to_sentence(:two_words_connector => ' ')
- end
-
- def test_one_element
- assert_equal "one", ['one'].to_sentence
- end
-
- def test_one_element_not_same_object
- elements = ["one"]
- assert_not_equal elements[0].object_id, elements.to_sentence.object_id
- end
-
- def test_one_non_string_element
- assert_equal '1', [1].to_sentence
- end
-
- def test_does_not_modify_given_hash
- options = { words_connector: ' ' }
- assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options)
- assert_equal({ words_connector: ' ' }, options)
- end
-
- def test_with_blank_elements
- assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence
- end
-end
-
-class ArrayExtToSTests < ActiveSupport::TestCase
- def test_to_s_db
- collection = [
- Class.new { def id() 1 end }.new,
- Class.new { def id() 2 end }.new,
- Class.new { def id() 3 end }.new
- ]
-
- assert_equal "null", [].to_s(:db)
- assert_equal "1,2,3", collection.to_s(:db)
- end
-end
-
-class ArrayExtGroupingTests < ActiveSupport::TestCase
- def setup
- Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
- end
-
- def teardown
- Fixnum.send :public, :/
- end
-
- def test_in_groups_of_with_perfect_fit
- groups = []
- ('a'..'i').to_a.in_groups_of(3) do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups
- assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3)
- end
-
- def test_in_groups_of_with_padding
- groups = []
- ('a'..'g').to_a.in_groups_of(3) do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups
- end
-
- def test_in_groups_of_pads_with_specified_values
- groups = []
-
- ('a'..'g').to_a.in_groups_of(3, 'foo') do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), ['g', 'foo', 'foo']], groups
- end
-
- def test_in_groups_of_without_padding
- groups = []
-
- ('a'..'g').to_a.in_groups_of(3, false) do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), ['g']], groups
- end
-
- def test_in_groups_returned_array_size
- array = (1..7).to_a
-
- 1.upto(array.size + 1) do |number|
- assert_equal number, array.in_groups(number).size
- end
- end
-
- def test_in_groups_with_empty_array
- assert_equal [[], [], []], [].in_groups(3)
- end
-
- def test_in_groups_with_block
- array = (1..9).to_a
- groups = []
-
- array.in_groups(3) do |group|
- groups << group
- end
-
- assert_equal array.in_groups(3), groups
- end
-
- def test_in_groups_with_perfect_fit
- assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
- (1..9).to_a.in_groups(3)
- end
-
- def test_in_groups_with_padding
- array = (1..7).to_a
-
- assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]],
- array.in_groups(3)
- assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']],
- array.in_groups(3, 'foo')
- end
-
- def test_in_groups_without_padding
- assert_equal [[1, 2, 3], [4, 5], [6, 7]],
- (1..7).to_a.in_groups(3, false)
- end
-end
-
-class ArraySplitTests < ActiveSupport::TestCase
- def test_split_with_empty_array
- assert_equal [[]], [].split(0)
- end
-
- def test_split_with_argument
- 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
- 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
- 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
-
-class ArrayToXmlTests < ActiveSupport::TestCase
- def test_to_xml
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 },
- { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
- ].to_xml(:skip_instruct => true, :indent => 0)
-
- assert_equal '<objects type="array"><object>', xml.first(30)
- assert xml.include?(%(<age type="integer">26</age>)), xml
- assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml
- assert xml.include?(%(<name>David</name>)), xml
- assert xml.include?(%(<age type="integer">31</age>)), xml
- assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml
- assert xml.include?(%(<name>Jason</name>)), xml
- end
-
- def test_to_xml_with_dedicated_name
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31 }
- ].to_xml(:skip_instruct => true, :indent => 0, :root => "people")
-
- assert_equal '<people type="array"><person>', xml.first(29)
- end
-
- def test_to_xml_with_options
- xml = [
- { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
- ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0)
-
- assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street-address>Paulina</street-address>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<street-address>Evergreen</street-address>))
- assert xml.include?(%(<name>Jason</name>))
- end
-
- def test_to_xml_with_dasherize_false
- xml = [
- { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
- ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false)
-
- assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street_address>Paulina</street_address>))
- assert xml.include?(%(<street_address>Evergreen</street_address>))
- end
-
- def test_to_xml_with_dasherize_true
- xml = [
- { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
- ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true)
-
- assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street-address>Paulina</street-address>))
- assert xml.include?(%(<street-address>Evergreen</street-address>))
- end
-
- def test_to_with_instruct
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 },
- { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
- ].to_xml(:skip_instruct => false, :indent => 0)
-
- assert_match(/^<\?xml [^>]*/, xml)
- assert_equal 0, xml.rindex(/<\?xml /)
- end
-
- def test_to_xml_with_block
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 },
- { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
- ].to_xml(:skip_instruct => true, :indent => 0) do |builder|
- builder.count 2
- end
-
- assert xml.include?(%(<count>2</count>)), xml
- end
-
- def test_to_xml_with_empty
- xml = [].to_xml
- assert_match(/type="array"\/>/, xml)
- end
-
- def test_to_xml_dups_options
- options = {:skip_instruct => true}
- [].to_xml(options)
- # :builder, etc, shouldn't be added to options
- assert_equal({:skip_instruct => true}, options)
- end
-end
-
-class ArrayExtractOptionsTests < ActiveSupport::TestCase
- class HashSubclass < Hash
- end
-
- class ExtractableHashSubclass < Hash
- def extractable_options?
- true
- end
- end
-
- def test_extract_options
- assert_equal({}, [].extract_options!)
- assert_equal({}, [1].extract_options!)
- assert_equal({:a=>:b}, [{:a=>:b}].extract_options!)
- assert_equal({:a=>:b}, [1, {:a=>:b}].extract_options!)
- end
-
- def test_extract_options_doesnt_extract_hash_subclasses
- hash = HashSubclass.new
- hash[:foo] = 1
- array = [hash]
- options = array.extract_options!
- assert_equal({}, options)
- assert_equal [hash], array
- end
-
- def test_extract_options_extracts_extractable_subclass
- hash = ExtractableHashSubclass.new
- hash[:foo] = 1
- array = [hash]
- options = array.extract_options!
- assert_equal({:foo => 1}, options)
- assert_equal [], array
- end
-
- def test_extract_options_extracts_hwia
- hash = [{:foo => 1}.with_indifferent_access]
- options = hash.extract_options!
- assert_equal 1, options[:foo]
- end
-end
-
-class ArrayWrapperTests < ActiveSupport::TestCase
- class FakeCollection
- def to_ary
- ["foo", "bar"]
- end
- end
-
- class Proxy
- def initialize(target) @target = target end
- def method_missing(*a) @target.send(*a) end
- end
-
- class DoubtfulToAry
- def to_ary
- :not_an_array
- end
- end
-
- class NilToAry
- def to_ary
- nil
- end
- end
-
- def test_array
- ary = %w(foo bar)
- assert_same ary, Array.wrap(ary)
- end
-
- def test_nil
- assert_equal [], Array.wrap(nil)
- end
-
- def test_object
- o = Object.new
- assert_equal [o], Array.wrap(o)
- end
-
- def test_string
- assert_equal ["foo"], Array.wrap("foo")
- end
-
- def test_string_with_newline
- assert_equal ["foo\nbar"], Array.wrap("foo\nbar")
- end
-
- def test_object_with_to_ary
- assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new)
- end
-
- def test_proxy_object
- p = Proxy.new(Object.new)
- assert_equal [p], Array.wrap(p)
- end
-
- def test_proxy_to_object_with_to_ary
- p = Proxy.new(FakeCollection.new)
- assert_equal [p], Array.wrap(p)
- end
-
- def test_struct
- o = Struct.new(:foo).new(123)
- assert_equal [o], Array.wrap(o)
- end
-
- def test_wrap_returns_wrapped_if_to_ary_returns_nil
- o = NilToAry.new
- assert_equal [o], Array.wrap(o)
- end
-
- def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array
- assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new)
- end
-end
-
-class ArrayPrependAppendTest < ActiveSupport::TestCase
- def test_append
- assert_equal [1, 2], [1].append(2)
- end
-
- def test_prepend
- assert_equal [2, 1], [1].prepend(2)
- 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 0e0742d147..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
@@ -40,7 +47,11 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
def test_simple_accessor_declaration_with_instance_reader_false
_instance_methods = single_class.public_instance_methods
- single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false
+
+ 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 !_instance_methods.include?(:no_instance_reader)
@@ -49,7 +60,9 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
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"
@@ -65,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
@@ -97,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 5d0af035cc..e89be25b53 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class DateExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,*args)
@@ -8,6 +9,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_yesterday_in_calendar_reform
assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday
@@ -349,22 +351,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
Date.new(2005,2,28).advance(options)
assert_equal({ :years => 3, :months => 11, :days => 2 }, options)
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
class DateExtBehaviorTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 0a40aeb96c..74319ecd09 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class DateTimeExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,hour,minute,second,*args)
@@ -8,6 +9,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_to_s
datetime = DateTime.new(2005, 2, 21, 14, 30, 0, 0)
@@ -162,6 +164,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9)
end
+ def test_advance_partial_days
+ assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5)
+ assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 0.5)
+ assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5, :months => 1)
+ end
+
def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas
# If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead.
assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1)
@@ -330,6 +338,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
def test_to_f
assert_equal 946684800.0, DateTime.civil(2000).to_f
assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f
+ assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f
end
def test_to_i
@@ -346,12 +355,4 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 0, DateTime.civil(2000).nsec
assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb
new file mode 100644
index 0000000000..08e0a1d6e1
--- /dev/null
+++ b/activesupport/test/core_ext/digest/uuid_test.rb
@@ -0,0 +1,24 @@
+require 'abstract_unit'
+require 'active_support/core_ext/digest/uuid'
+
+class DigestUUIDExt < ActiveSupport::TestCase
+ def test_v3_uuids
+ assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com")
+ assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com")
+ assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3")
+ assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::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", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com")
+ assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com")
+ assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3")
+ assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US")
+ end
+
+ def test_invalid_hash_class
+ assert_raise ArgumentError do
+ Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, '1.2.3')
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 28ba33331e..31af3c4521 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -2,8 +2,11 @@ require 'abstract_unit'
require 'active_support/inflector'
require 'active_support/time'
require 'active_support/json'
+require 'time_zone_test_helpers'
class DurationTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_is_a
d = 1.day
assert d.is_a?(ActiveSupport::Duration)
@@ -31,6 +34,13 @@ class DurationTest < ActiveSupport::TestCase
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
@@ -152,6 +162,10 @@ class DurationTest < ActiveSupport::TestCase
assert_equal counter, 60
end
+ def test_as_json
+ assert_equal 172800, 2.days.as_json
+ end
+
def test_to_json
assert_equal '172800', 2.days.to_json
end
@@ -160,12 +174,4 @@ class DurationTest < ActiveSupport::TestCase
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
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
new file mode 100644
index 0000000000..a7e12117f3
--- /dev/null
+++ b/activesupport/test/core_ext/hash/transform_keys_test.rb
@@ -0,0 +1,32 @@
+require 'abstract_unit'
+require 'active_support/core_ext/hash/keys'
+
+class TransformKeysTest < ActiveSupport::TestCase
+ test "transform_keys returns a new hash with the keys computed from the block" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_keys { |k| "#{k}!".to_sym }
+
+ assert_equal({ a: 'a', b: 'b' }, original)
+ assert_equal({ a!: 'a', b!: 'b' }, mapped)
+ end
+
+ test "transform_keys! modifies the keys of the original" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_keys! { |k| "#{k}!".to_sym }
+
+ assert_equal({ a!: 'a', b!: 'b' }, original)
+ assert_same original, mapped
+ end
+
+ test "transform_keys returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_keys
+ assert_equal Enumerator, enumerator.class
+ end
+
+ test "transform_keys is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym }
+ assert_equal({ a0: 'a', b1: 'b' }, mapped)
+ end
+end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
new file mode 100644
index 0000000000..45ed11fef7
--- /dev/null
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -0,0 +1,61 @@
+require 'abstract_unit'
+require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/hash/transform_values'
+
+class TransformValuesTest < ActiveSupport::TestCase
+ test "transform_values returns a new hash with the values computed from the block" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal({ a: 'a', b: 'b' }, original)
+ assert_equal({ a: 'a!', b: 'b!' }, mapped)
+ end
+
+ test "transform_values! modifies the values of the original" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_values! { |v| v + '!' }
+
+ assert_equal({ a: 'a!', b: 'b!' }, original)
+ assert_same original, mapped
+ end
+
+ test "indifferent access is still indifferent after mapping values" do
+ original = { a: 'a', b: 'b' }.with_indifferent_access
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal 'a!', mapped[:a]
+ assert_equal 'a!', mapped['a']
+ end
+
+ # This is to be consistent with the behavior of Ruby's built in methods
+ # (e.g. #select, #reject) as of 2.2
+ test "default values do not persist during mapping" do
+ original = Hash.new('foo')
+ original[:a] = 'a'
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal 'a!', mapped[:a]
+ assert_nil mapped[:b]
+ end
+
+ test "default procs do not persist after mapping" do
+ original = Hash.new { 'foo' }
+ original[:a] = 'a'
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal 'a!', mapped[:a]
+ assert_nil mapped[:b]
+ end
+
+ test "transform_values returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_values
+ assert_equal Enumerator, enumerator.class
+ end
+
+ test "transform_values is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_values.with_index { |v, i| [v, i].join }
+ assert_equal({ a: 'a0', b: 'b1' }, mapped)
+ end
+end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index 40c8f03374..5e9fdfd872 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 } } }
@@ -36,6 +46,10 @@ class HashExtTest < ActiveSupport::TestCase
@nested_illegal_symbols = { [] => { [] => 3} }
@upcase_strings = { 'A' => 1, 'B' => 2 }
@nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } }
+ @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] }
+ @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] }
+ @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] }
+ @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] }
end
def test_methods
@@ -56,6 +70,8 @@ class HashExtTest < ActiveSupport::TestCase
assert_respond_to h, :to_options!
assert_respond_to h, :compact
assert_respond_to h, :compact!
+ assert_respond_to h, :except
+ assert_respond_to h, :except!
end
def test_transform_keys
@@ -74,6 +90,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
end
def test_deep_transform_keys_not_mutates
@@ -99,6 +118,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
end
def test_deep_transform_keys_with_bang_mutates
@@ -124,6 +146,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_symbols, @nested_symbols.deep_symbolize_keys
assert_equal @nested_symbols, @nested_strings.deep_symbolize_keys
assert_equal @nested_symbols, @nested_mixed.deep_symbolize_keys
+ assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_symbolize_keys
+ assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_symbolize_keys
+ assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_symbolize_keys
end
def test_deep_symbolize_keys_not_mutates
@@ -149,6 +174,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_symbols, @nested_symbols.deep_dup.deep_symbolize_keys!
assert_equal @nested_symbols, @nested_strings.deep_dup.deep_symbolize_keys!
assert_equal @nested_symbols, @nested_mixed.deep_dup.deep_symbolize_keys!
+ assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_dup.deep_symbolize_keys!
+ assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_symbolize_keys!
+ assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_symbolize_keys!
end
def test_deep_symbolize_keys_with_bang_mutates
@@ -194,6 +222,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_strings, @nested_symbols.deep_stringify_keys
assert_equal @nested_strings, @nested_strings.deep_stringify_keys
assert_equal @nested_strings, @nested_mixed.deep_stringify_keys
+ assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_stringify_keys
+ assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_stringify_keys
+ assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_stringify_keys
end
def test_deep_stringify_keys_not_mutates
@@ -219,6 +250,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_strings, @nested_symbols.deep_dup.deep_stringify_keys!
assert_equal @nested_strings, @nested_strings.deep_dup.deep_stringify_keys!
assert_equal @nested_strings, @nested_mixed.deep_dup.deep_stringify_keys!
+ assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_dup.deep_stringify_keys!
+ assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_stringify_keys!
+ assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_stringify_keys!
end
def test_deep_stringify_keys_with_bang_mutates
@@ -411,6 +445,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'
@@ -430,6 +470,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
@@ -442,6 +488,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
@@ -528,6 +586,8 @@ class HashExtTest < ActiveSupport::TestCase
roundtrip = mixed_with_default.with_indifferent_access.to_hash
assert_equal @strings, roundtrip
assert_equal '1234', roundtrip.default
+
+ # Ensure nested hashes are not HashWithIndiffereneAccess
new_to_hash = @nested_mixed.with_indifferent_access.to_hash
assert_not new_to_hash.instance_of?(HashWithIndifferentAccess)
assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess)
@@ -601,7 +661,15 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 1, h['first']
end
- def test_indifferent_subhashes
+ def test_to_options_on_indifferent_preserves_works_as_hash_with_dup
+ h = HashWithIndifferentAccess.new({ a: { b: 'b' } })
+ dup = h.dup
+
+ dup[:a][:c] = 'c'
+ assert_equal 'c', h[:a][:c]
+ end
+
+ 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"}}
@@ -625,6 +693,11 @@ class HashExtTest < ActiveSupport::TestCase
{ :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
{ :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
+ # not all valid keys are required to be present
+ assert_nothing_raised do
+ { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny, :sunny ])
+ { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny, :sunny)
+ end
exception = assert_raise ArgumentError do
{ :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
@@ -635,6 +708,16 @@ class HashExtTest < ActiveSupport::TestCase
{ :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 ])
+ end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
+
+ exception = assert_raise ArgumentError do
+ { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure)
+ end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
end
def test_assorted_keys_not_stringified
@@ -663,6 +746,16 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal expected, hash_1
end
+ def test_deep_merge_with_falsey_values
+ hash_1 = { e: false }
+ hash_2 = { e: 'e' }
+ expected = { e: [:e, false, 'e'] }
+ assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] })
+
+ hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] }
+ assert_equal expected, hash_1
+ end
+
def test_deep_merge_on_indifferent_access
hash_1 = HashWithIndifferentAccess.new({ :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } })
hash_2 = HashWithIndifferentAccess.new({ :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } })
@@ -853,13 +946,19 @@ class HashExtTest < ActiveSupport::TestCase
def test_except_with_more_than_one_argument
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x' }
+
assert_equal expected, original.except(:b, :c)
+
+ assert_equal expected, original.except!(:b, :c)
+ assert_equal expected, original
end
def test_except_with_original_frozen
original = { :a => 'x', :b => 'y' }
original.freeze
assert_nothing_raised { original.except(:a) }
+
+ assert_raise(RuntimeError) { original.except!(:a) }
end
def test_except_with_mocha_expectation_on_original
@@ -871,11 +970,11 @@ class HashExtTest < ActiveSupport::TestCase
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)
@@ -884,15 +983,21 @@ class HashExtTest < ActiveSupport::TestCase
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
@@ -1397,12 +1502,32 @@ class HashToXmlTest < ActiveSupport::TestCase
end
end
+ def test_from_xml_array_one
+ expected = { 'numbers' => { 'type' => 'Array', 'value' => '1' }}
+ assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value></numbers>')
+ end
+
+ def test_from_xml_array_many
+ expected = { 'numbers' => { 'type' => 'Array', 'value' => [ '1', '2' ] }}
+ assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value><value>2</value></numbers>')
+ end
+
def test_from_trusted_xml_allows_symbol_and_yaml_types
expected = { 'product' => { 'name' => :value }}
assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>')
assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>')
end
+ def test_should_use_default_proc_for_unknown_key
+ hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
+ assert_equal 3, hash_wia[:new_key]
+ end
+
+ def test_should_use_default_proc_if_no_key_is_supplied
+ hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
+ assert_equal 3, hash_wia.default
+ end
+
def test_should_use_default_value_for_unknown_key
hash_wia = HashWithIndifferentAccess.new(3)
assert_equal 3, hash_wia[:new_key]
@@ -1430,6 +1555,17 @@ class HashToXmlTest < ActiveSupport::TestCase
assert_equal 3, hash_wia.default
end
+ def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access
+ hash = Hash.new do
+ 2 + 1
+ end
+ assert_equal 3, hash[:foo]
+
+ hash_wia = hash.with_indifferent_access
+ assert_equal 3, hash_wia[:foo]
+ assert_equal 3, hash_wia[:bar]
+ end
+
# The XML builder seems to fail miserably when trying to tag something
# with the same name as a Kernel method (throw, test, loop, select ...)
def test_kernel_method_names_to_xml
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..478a00d2d2
--- /dev/null
+++ b/activesupport/test/core_ext/kernel/concern_test.rb
@@ -0,0 +1,13 @@
+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_not Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect
+ ensure
+ 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 18b251173f..a87af0007c 100644
--- a/activesupport/test/core_ext/kernel_test.rb
+++ b/activesupport/test/core_ext/kernel_test.rb
@@ -30,14 +30,6 @@ class KernelTest < ActiveSupport::TestCase
end
- def test_silence_stderr
- old_stderr_position = STDERR.tell
- silence_stderr { STDERR.puts 'hello world' }
- assert_equal old_stderr_position, STDERR.tell
- rescue Errno::ESPIPE
- # Skip if we can't STDERR.tell
- end
-
def test_silence_stream
old_stream_position = STDOUT.tell
silence_stream(STDOUT) { STDOUT.puts 'hello world' }
@@ -56,9 +48,11 @@ class KernelTest < ActiveSupport::TestCase
def test_quietly
old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell
- quietly do
- puts 'see me, feel me'
- STDERR.puts 'touch me, heal me'
+ assert_deprecated do
+ quietly do
+ puts 'see me, feel me'
+ STDERR.puts 'touch me, heal me'
+ end
end
assert_equal old_stdout_position, STDOUT.tell
assert_equal old_stderr_position, STDERR.tell
@@ -66,10 +60,6 @@ class KernelTest < ActiveSupport::TestCase
# Skip if we can't STDERR.tell
end
- def test_silence_stderr_with_return_value
- assert_equal 1, silence_stderr { 1 }
- end
-
def test_class_eval
o = Object.new
class << o; @x = 1; end
@@ -77,10 +67,18 @@ class KernelTest < ActiveSupport::TestCase
end
def test_capture
- assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' }
- assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' }
- assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') }
- assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') }
+ assert_deprecated do
+ assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' }
+ end
+ assert_deprecated do
+ assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' }
+ end
+ assert_deprecated do
+ assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') }
+ end
+ assert_deprecated do
+ assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') }
+ end
end
end
@@ -137,4 +135,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/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb
index 31863d0aca..5f804c749b 100644
--- a/activesupport/test/core_ext/load_error_test.rb
+++ b/activesupport/test/core_ext/load_error_test.rb
@@ -14,6 +14,15 @@ class TestMissingSourceFile < ActiveSupport::TestCase
assert_equal 'nor/this/one.rb', e.path
end
end
+
+ def test_is_missing
+ begin load 'nor_does_this_one'
+ rescue MissingSourceFile => e
+ assert e.is_missing?('nor_does_this_one')
+ assert e.is_missing?('nor_does_this_one.rb')
+ assert_not e.is_missing?('some_other_file')
+ end
+ end
end
class TestLoadError < ActiveSupport::TestCase
@@ -29,4 +38,4 @@ class TestLoadError < ActiveSupport::TestCase
assert_equal 'nor/this/one.rb', e.path
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
index c6863b24a4..07d860b71c 100644
--- a/activesupport/test/core_ext/module/concerning_test.rb
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -1,35 +1,65 @@
require 'abstract_unit'
require 'active_support/core_ext/module/concerning'
-class ConcerningTest < ActiveSupport::TestCase
- def test_concern_shortcut_creates_a_module_but_doesnt_include_it
- mod = Module.new { concern(:Foo) { } }
- assert_kind_of Module, mod::Foo
- assert mod::Foo.respond_to?(:included)
- assert !mod.ancestors.include?(mod::Foo), mod.ancestors.inspect
+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 :Foo do
+ concern :Baz do
included { @foo = 1 }
def should_be_public; end
end
end
# Declares a concern but doesn't include it
- assert_kind_of Module, klass::Foo
- assert !klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+ 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::Foo.public_instance_methods.map(&:to_s).include?('should_be_public')
+ assert klass::Baz.public_instance_methods.map(&:to_s).include?('should_be_public')
# Calls included hook
- assert_equal 1, Class.new { include klass::Foo }.instance_variable_get('@foo')
+ assert_equal 1, Class.new { include klass::Baz }.instance_variable_get('@foo')
end
- def test_concerning_declares_a_concern_and_includes_it_immediately
- klass = Class.new { concerning(:Foo) { } }
- assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+ 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 ff6e21854e..380f5ad42b 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -72,7 +72,7 @@ Product = Struct.new(:name) do
def type
@type ||= begin
- nil.type_name
+ :thing_without_same_method_name_as_delegated.name
end
end
end
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 3b1dabea8d..b82448458d 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -22,18 +22,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
end
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
-
- 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
assert_equal @now.advance(:days => 3000), 3000.days.since(@now)
assert_equal @now.advance(:months => 1), 1.month.since(@now)
@@ -83,44 +71,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year
assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year
end
-
- def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
- Time.zone = nil
- with_env_tz 'US/Eastern' do
- Time.stubs(:now).returns Time.local(2000)
- # 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_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
-
- def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'US/Eastern' do
- Time.stubs(:now).returns Time.local(2000)
- # since
- 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_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
- end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class NumericExtDateTest < ActiveSupport::TestCase
@@ -435,7 +385,7 @@ 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
diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb
new file mode 100644
index 0000000000..e68b1d23cb
--- /dev/null
+++ b/activesupport/test/core_ext/object/acts_like_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class ObjectTests < ActiveSupport::TestCase
+ class DuckTime
+ def acts_like_time?
+ true
+ end
+ end
+
+ def test_duck_typing
+ object = Object.new
+ time = Time.now
+ date = Date.today
+ dt = DateTime.new
+ duck = DuckTime.new
+
+ assert !object.acts_like?(:time)
+ assert !object.acts_like?(:date)
+
+ assert time.acts_like?(:time)
+ assert !time.acts_like?(:date)
+
+ assert !date.acts_like?(:time)
+ assert date.acts_like?(:date)
+
+ assert dt.acts_like?(:time)
+ assert dt.acts_like?(:date)
+
+ assert duck.acts_like?(:time)
+ assert !duck.acts_like?(:date)
+ end
+end
diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb
index 246bc7fa61..246bc7fa61 100644
--- a/activesupport/test/core_ext/blank_test.rb
+++ b/activesupport/test/core_ext/object/blank_test.rb
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..32d512eca3 100644
--- a/activesupport/test/core_ext/object/inclusion_test.rb
+++ b/activesupport/test/core_ext/object/inclusion_test.rb
@@ -37,14 +37,23 @@ class InTest < ActiveSupport::TestCase
end
class C < B
end
+ class D
+ end
def test_in_module
assert A.in?(B)
assert A.in?(C)
assert !A.in?(A)
+ assert !A.in?(D)
end
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 ))
+ assert_raise(ArgumentError) { 1.presence_in(1) }
+ end
end
diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb
new file mode 100644
index 0000000000..9f4c5dc4f1
--- /dev/null
+++ b/activesupport/test/core_ext/object/instance_variables_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class ObjectInstanceVariableTest < ActiveSupport::TestCase
+ def setup
+ @source, @dest = Object.new, Object.new
+ @source.instance_variable_set(:@bar, 'bar')
+ @source.instance_variable_set(:@baz, 'baz')
+ end
+
+ def test_instance_variable_names
+ assert_equal %w(@bar @baz), @source.instance_variable_names.sort
+ end
+
+ def test_instance_values
+ assert_equal({'bar' => 'bar', 'baz' => 'baz'}, @source.instance_values)
+ end
+
+ def test_instance_exec_passes_arguments_to_block
+ assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] }
+ end
+
+ def test_instance_exec_with_frozen_obj
+ assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] }
+ end
+
+ def test_instance_exec_nested
+ assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg|
+ [arg] + instance_exec('bar') { |v| [reverse, v] } }
+ end
+end
diff --git a/activesupport/test/core_ext/object/itself_test.rb b/activesupport/test/core_ext/object/itself_test.rb
new file mode 100644
index 0000000000..65db0ddf40
--- /dev/null
+++ b/activesupport/test/core_ext/object/itself_test.rb
@@ -0,0 +1,9 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class Object::ItselfTest < ActiveSupport::TestCase
+ test 'itself returns self' do
+ object = 'fun'
+ assert_equal object, object.itself
+ end
+end
diff --git a/activesupport/test/core_ext/object/json_cherry_pick_test.rb b/activesupport/test/core_ext/object/json_cherry_pick_test.rb
new file mode 100644
index 0000000000..2f7ea3a497
--- /dev/null
+++ b/activesupport/test/core_ext/object/json_cherry_pick_test.rb
@@ -0,0 +1,42 @@
+require 'abstract_unit'
+
+# These test cases were added to test that cherry-picking the json extensions
+# works correctly, primarily for dependencies problems reported in #16131. They
+# need to be executed in isolation to reproduce the scenario correctly, because
+# other test cases might have already loaded additional dependencies.
+
+class JsonCherryPickTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def test_time_as_json
+ require_or_skip 'active_support/core_ext/object/json'
+
+ expected = Time.new(2004, 7, 25)
+ actual = Time.parse(expected.as_json)
+
+ assert_equal expected, actual
+ end
+
+ def test_date_as_json
+ require_or_skip 'active_support/core_ext/object/json'
+
+ expected = Date.new(2004, 7, 25)
+ actual = Date.parse(expected.as_json)
+
+ assert_equal expected, actual
+ end
+
+ def test_datetime_as_json
+ require_or_skip 'active_support/core_ext/object/json'
+
+ expected = DateTime.new(2004, 7, 25)
+ actual = DateTime.parse(expected.as_json)
+
+ assert_equal expected, actual
+ end
+
+ private
+ def require_or_skip(file)
+ require(file) || skip("'#{file}' was already loaded")
+ end
+end
diff --git a/activesupport/test/core_ext/object/json_test.rb b/activesupport/test/core_ext/object/json_test.rb
deleted file mode 100644
index d3d31530df..0000000000
--- a/activesupport/test/core_ext/object/json_test.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'abstract_unit'
-
-class JsonTest < ActiveSupport::TestCase
- # See activesupport/test/json/encoding_test.rb for JSON encoding tests
-
- def test_deprecated_require_to_json_rb
- assert_deprecated { require 'active_support/core_ext/object/to_json' }
- end
-end
diff --git a/activesupport/test/core_ext/object/to_param_test.rb b/activesupport/test/core_ext/object/to_param_test.rb
index bd7c6c422a..30a7557dc2 100644
--- a/activesupport/test/core_ext/object/to_param_test.rb
+++ b/activesupport/test/core_ext/object/to_param_test.rb
@@ -2,6 +2,12 @@ require 'abstract_unit'
require 'active_support/core_ext/object/to_param'
class ToParamTest < ActiveSupport::TestCase
+ class CustomString < String
+ def to_param
+ "custom-#{ self }"
+ end
+ end
+
def test_object
foo = Object.new
def foo.to_s; 'foo' end
@@ -16,4 +22,16 @@ class ToParamTest < ActiveSupport::TestCase
assert_equal true, true.to_param
assert_equal false, false.to_param
end
+
+ def test_array
+ # Empty Array
+ assert_equal '', [].to_param
+
+ array = [1, 2, 3, 4]
+ assert_equal "1/2/3/4", array.to_param
+
+ # Array of different objects
+ array = [1, '3', { a: 1, b: 2 }, nil, true, false, CustomString.new('object')]
+ assert_equal "1/3/a=1&b=2//true/false/custom-object", array.to_param
+ 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 f887a9e613..09cab3ed35 100644
--- a/activesupport/test/core_ext/object/to_query_test.rb
+++ b/activesupport/test/core_ext/object/to_query_test.rb
@@ -46,19 +46,35 @@ class ToQueryTest < ActiveSupport::TestCase
:person => {:id => [20, 10]}
end
+ def test_empty_array
+ assert_equal "person%5B%5D=", [].to_query('person')
+ end
+
def test_nested_empty_hash
assert_equal '',
{}.to_query
- assert_query_equal 'a=1&b%5Bc%5D=3&b%5Bd%5D=',
+ 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%5Bk%5D=',
+ assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=',
{ b: { c: 3, k: {}, f: '' } }
- assert_query_equal 'a%5B%5D=&b=3',
+ assert_query_equal 'b=3',
{a: [], b: 3}
end
+ def test_hash_with_namespace
+ hash = { name: 'Nakshay', nationality: 'Indian' }
+ assert_equal "user%5Bname%5D=Nakshay&user%5Bnationality%5D=Indian", hash.to_query('user')
+ end
+
+ def test_hash_sorted_lexicographically
+ hash = { type: 'human', name: 'Nakshay' }
+ assert_equal "name=Nakshay&type=human", hash.to_query
+ 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/try_test.rb
index 0f454fdd95..8b754ced53 100644
--- a/activesupport/test/core_ext/object_and_class_ext_test.rb
+++ b/activesupport/test/core_ext/object/try_test.rb
@@ -1,70 +1,5 @@
require 'abstract_unit'
-require 'active_support/time'
require 'active_support/core_ext/object'
-require 'active_support/core_ext/class/subclasses'
-
-class ObjectTests < ActiveSupport::TestCase
- class DuckTime
- def acts_like_time?
- true
- end
- end
-
- def test_duck_typing
- object = Object.new
- time = Time.now
- date = Date.today
- dt = DateTime.new
- duck = DuckTime.new
-
- assert !object.acts_like?(:time)
- assert !object.acts_like?(:date)
-
- assert time.acts_like?(:time)
- assert !time.acts_like?(:date)
-
- assert !date.acts_like?(:time)
- assert date.acts_like?(:date)
-
- assert dt.acts_like?(:time)
- assert dt.acts_like?(:date)
-
- assert duck.acts_like?(:time)
- assert !duck.acts_like?(:date)
- end
-end
-
-class ObjectInstanceVariableTest < ActiveSupport::TestCase
- def setup
- @source, @dest = Object.new, Object.new
- @source.instance_variable_set(:@bar, 'bar')
- @source.instance_variable_set(:@baz, 'baz')
- end
-
- def test_instance_variable_names
- assert_equal %w(@bar @baz), @source.instance_variable_names.sort
- end
-
- def test_instance_values
- object = Object.new
- object.instance_variable_set :@a, 1
- object.instance_variable_set :@b, 2
- assert_equal({'a' => 1, 'b' => 2}, object.instance_values)
- end
-
- def test_instance_exec_passes_arguments_to_block
- assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] }
- end
-
- def test_instance_exec_with_frozen_obj
- assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] }
- end
-
- def test_instance_exec_nested
- assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg|
- [arg] + instance_exec('bar') { |v| [reverse, v] } }
- end
-end
class ObjectTryTest < ActiveSupport::TestCase
def setup
@@ -141,7 +76,7 @@ class ObjectTryTest < ActiveSupport::TestCase
assert_raise(NoMethodError) { klass.new.try!(:private_method) }
end
-
+
def test_try_with_private_method
klass = Class.new do
private
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index 150e6b65fb..98c4ec6b5e 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -12,10 +12,11 @@ class RangeTest < ActiveSupport::TestCase
date_range = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30)
assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db)
end
-
+
def test_date_range
assert_instance_of Range, DateTime.new..DateTime.new
assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new
+ assert_instance_of Range, DateTime.new..DateTime::Infinity.new
end
def test_overlaps_last_inclusive
@@ -116,4 +117,9 @@ class RangeTest < ActiveSupport::TestCase
datetime = DateTime.now
assert ((datetime - 1.hour)..datetime).each {}
end
+
+ def test_date_time_with_step
+ datetime = DateTime.now
+ assert ((datetime - 1.hour)..datetime).step(1) {}
+ end
end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 072b970a2d..d77e6be595 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -10,10 +10,12 @@ require 'active_support/time'
require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/indent'
+require 'time_zone_test_helpers'
class StringInflectionsTest < ActiveSupport::TestCase
include InflectorTestCases
include ConstantizeTestCases
+ include TimeZoneTestHelpers
def test_strip_heredoc_on_an_empty_string
assert_equal '', ''.strip_heredoc
@@ -58,6 +60,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)
@@ -209,19 +216,40 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal "Hello Wor...", "Hello World!!".truncate(12)
end
- def test_truncate_with_omission_and_seperator
+ def test_truncate_with_omission_and_separator
assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]")
assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ')
assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ')
assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ')
end
- def test_truncate_with_omission_and_regexp_seperator
+ def test_truncate_with_omission_and_regexp_separator
assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/)
assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/)
assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/)
end
+ def test_truncate_words
+ assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3)
+ assert_equal "Hello Big...", "Hello Big World!".truncate_words(2)
+ end
+
+ def test_truncate_words_with_omission
+ assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, :omission => "[...]")
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, :omission => "[...]")
+ end
+
+ def test_truncate_words_with_separator
+ assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, :separator => '<br>')
+ assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :separator => '<br>')
+ assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, :separator => '<br>')
+ end
+
+ def test_truncate_words_with_separator_and_omission
+ assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, :omission => "[...]", :separator => '<br>')
+ assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>')
+ end
+
def test_truncate_multibyte
assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8),
"\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10)
@@ -296,6 +324,12 @@ class StringAccessTest < ActiveSupport::TestCase
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
@@ -308,6 +342,12 @@ class StringAccessTest < ActiveSupport::TestCase
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
@@ -337,6 +377,8 @@ class StringAccessTest < ActiveSupport::TestCase
end
class StringConversionsTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_string_to_time
with_env_tz "Europe/Moscow" do
assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:utc)
@@ -506,14 +548,6 @@ class StringConversionsTest < ActiveSupport::TestCase
assert_nil "".to_date
assert_equal Date.new(Date.today.year, 2, 3), "Feb 3rd".to_date
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class StringBehaviourTest < ActiveSupport::TestCase
@@ -608,6 +642,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"
@@ -718,6 +775,14 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = "<b>hello</b>".html_safe
assert_equal string, ERB::Util.html_escape(string)
end
+
+ test "ERB::Util.html_escape_once only escapes once" do
+ string = '1 < 2 &amp; 3'
+ escaped_string = "1 &lt; 2 &amp; 3"
+
+ assert_equal escaped_string, ERB::Util.html_escape_once(string)
+ assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string)
+ end
end
class StringExcludeTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index e0a4b1be3e..c8283cddc5 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class TimeExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,hour,minute,second,usec=0)
@@ -8,6 +9,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_seconds_since_midnight
assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight
@@ -847,15 +849,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
def test_all_year
assert_equal Time.local(2011,1,1,0,0,0)..Time.local(2011,12,31,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_year
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
end
class 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 7fe4d4a6b2..3000da8da4 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -1,7 +1,9 @@
require 'abstract_unit'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TimeWithZoneTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
def setup
@utc = Time.utc(2000, 1, 1, 0)
@@ -246,16 +248,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 1)
end
+ def test_minus_with_time_precision
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ end
+
def test_minus_with_time_with_zone
twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] )
twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] )
assert_equal 86_400.0, twz2 - twz1
end
+ def test_minus_with_time_with_zone_precision
+ twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone['UTC'] )
+ twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] )
+ assert_equal 86_399.999999998, twz2 - twz1
+ end
+
def test_minus_with_datetime
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
end
+ def test_minus_with_datetime_precision
+ assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
+ end
+
def test_minus_with_wrapped_datetime
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1)
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
@@ -350,6 +367,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_acts_like_time
+ assert @twz.acts_like_time?
assert @twz.acts_like?(:time)
assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time)
end
@@ -795,23 +813,17 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "undefined method `this_method_does_not_exist' for Fri, 31 Dec 1999 19:00:00 EST -05:00:Time", e.message
assert_no_match "rescue", e.backtrace.first
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
- @t, @dt = Time.utc(2000), DateTime.civil(2000)
+ @t, @dt, @zone = Time.utc(2000), DateTime.civil(2000), Time.zone
end
def teardown
- Time.zone = nil
+ Time.zone = @zone
end
def test_in_time_zone
@@ -867,8 +879,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
def test_localtime
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal
- ensure
- Time.zone = nil
end
def test_use_zone
@@ -907,6 +917,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_time_zone_getter_and_setter_with_zone_default_set
+ old_zone_default = Time.zone_default
Time.zone_default = ActiveSupport::TimeZone['Alaska']
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
Time.zone = ActiveSupport::TimeZone['Hawaii']
@@ -914,8 +925,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
Time.zone = nil
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
ensure
- Time.zone = nil
- Time.zone_default = nil
+ Time.zone_default = old_zone_default
end
def test_time_zone_setter_is_thread_safe
@@ -987,8 +997,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name
assert_equal Time.utc(2000), Time.current.time
end
- ensure
- Time.zone = nil
end
def test_time_in_time_zone_doesnt_affect_receiver
@@ -999,25 +1007,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
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
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
@d = Date.civil(2000)
end
- def teardown
- Time.zone = nil
- end
-
def test_in_time_zone
with_tz_default 'Alaska' do
assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone.inspect
@@ -1050,28 +1048,17 @@ class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
assert_raise(ArgumentError) { @d.in_time_zone(-15.hours) }
assert_raise(ArgumentError) { @d.in_time_zone(Object.new) }
end
-
- protected
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
class TimeWithZoneMethodsForString < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
@s = "Sat, 01 Jan 2000 00:00:00"
@u = "Sat, 01 Jan 2000 00:00:00 UTC +00:00"
@z = "Fri, 31 Dec 1999 19:00:00 EST -05:00"
end
- def teardown
- Time.zone = nil
- end
-
def test_in_time_zone
with_tz_default 'Alaska' do
assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone.inspect
@@ -1126,13 +1113,4 @@ class TimeWithZoneMethodsForString < ActiveSupport::TestCase
assert_raise(ArgumentError) { @u.in_time_zone(Object.new) }
assert_raise(ArgumentError) { @z.in_time_zone(Object.new) }
end
-
- protected
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb
index 03e388dd7a..43a5997ddd 100644
--- a/activesupport/test/core_ext/uri_ext_test.rb
+++ b/activesupport/test/core_ext/uri_ext_test.rb
@@ -7,7 +7,7 @@ class URIExtTest < ActiveSupport::TestCase
def test_uri_decode_handle_multibyte
str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
- parser = URI::Parser.new
+ parser = URI.parser
assert_equal str, parser.unescape(parser.escape(str))
end
end
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 4ca63b3417..5fc3de651a 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -47,18 +47,22 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_tracking_loaded_files
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_two'
- assert_equal 2, ActiveSupport::Dependencies.loaded.size
+ with_loading do
+ require_dependency 'dependencies/service_one'
+ require_dependency 'dependencies/service_two'
+ assert_equal 2, ActiveSupport::Dependencies.loaded.size
+ end
ensure
Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne)
Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo)
end
def test_tracking_identical_loaded_files
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_one'
- assert_equal 1, ActiveSupport::Dependencies.loaded.size
+ with_loading do
+ require_dependency 'dependencies/service_one'
+ require_dependency 'dependencies/service_one'
+ assert_equal 1, ActiveSupport::Dependencies.loaded.size
+ end
ensure
Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne)
end
@@ -367,9 +371,11 @@ class DependenciesTest < ActiveSupport::TestCase
with_autoloading_fixtures do
e = assert_raise(NameError) { A::DoesNotExist.nil? }
assert_equal "uninitialized constant A::DoesNotExist", e.message
+ assert_equal :DoesNotExist, e.name
e = assert_raise(NameError) { A::B::DoesNotExist.nil? }
assert_equal "uninitialized constant A::B::DoesNotExist", e.message
+ assert_equal :DoesNotExist, e.name
end
end
@@ -537,6 +543,7 @@ class DependenciesTest < ActiveSupport::TestCase
mod = Module.new
e = assert_raise(NameError) { mod::E }
assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message
+ assert_equal :E, e.name
end
end
@@ -954,7 +961,7 @@ class DependenciesTest < ActiveSupport::TestCase
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
@@ -987,11 +994,4 @@ class DependenciesTest < ActiveSupport::TestCase
ensure
ActiveSupport::Dependencies.hook!
end
-
-private
- def remove_constants(*constants)
- constants.each do |constant|
- Object.send(:remove_const, constant) if Object.const_defined?(constant)
- end
- end
end
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index ee1c69502e..7aff56cbad 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -355,4 +355,21 @@ class DeprecationTest < ActiveSupport::TestCase
end
deprecator
end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
end
diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb
index 5ef59b6e6b..3faa15e7fd 100644
--- a/activesupport/test/i18n_test.rb
+++ b/activesupport/test/i18n_test.rb
@@ -99,7 +99,6 @@ class I18nTest < ActiveSupport::TestCase
end
def test_to_sentence_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
assert_equal 'a, b, and c', %w[a b c].to_sentence(locale: 'empty')
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 35967ba656..58fdea0972 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -200,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
@@ -497,10 +498,10 @@ class InflectorTest < ActiveSupport::TestCase
end
%w(plurals singulars uncountables humans acronyms).each do |scope|
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_clear_inflections_with_#{scope}") do
- with_dup do
- # clear the inflections
+ define_method("test_clear_inflections_with_#{scope}") do
+ with_dup do
+ # clear the inflections
+ ActiveSupport::Inflector.inflections do |inflect|
inflect.clear(scope)
assert_equal [], inflect.send(scope)
end
@@ -508,6 +509,14 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ def test_inflections_with_uncountable_words
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable "HTTP"
+ end
+
+ assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP")
+ end
+
# Dups the singleton and yields, restoring the original inflections later.
# Use this in tests what modify the state of the singleton.
#
@@ -515,9 +524,10 @@ class InflectorTest < ActiveSupport::TestCase
# there are module functions that access ActiveSupport::Inflector.inflections,
# so we need to replace the singleton itself.
def with_dup
- original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)
- ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original.dup)
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+ yield
ensure
- ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original)
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
end
end
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index 4bd1b2e47c..b556da0046 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -208,9 +208,11 @@ module InflectorTestCases
}
UnderscoreToHuman = {
- "employee_salary" => "Employee salary",
- "employee_id" => "Employee",
- "underground" => "Underground"
+ 'employee_salary' => 'Employee salary',
+ 'employee_id' => 'Employee',
+ 'underground' => 'Underground',
+ '_id' => 'Id',
+ '_external_id' => 'External'
}
UnderscoreToHumanWithoutCapitalize = {
@@ -314,7 +316,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 07d7e530ca..80bf255080 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -73,22 +73,20 @@ class TestJSONDecoding < ActiveSupport::TestCase
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), "JSON decoding \
- failed for #{json}"
+ with_parse_json_times(true) do
+ silence_warnings do
+ assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
+ failed for #{json}"
+ end
end
- ActiveSupport.parse_json_times = prev
end
end
test "json decodes time json with time parsing disabled" do
- prev = ActiveSupport.parse_json_times
- ActiveSupport.parse_json_times = false
- expected = {"a" => "2007-01-01 01:12:34 Z"}
- assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
- ActiveSupport.parse_json_times = prev
+ with_parse_json_times(false) do
+ expected = {"a" => "2007-01-01 01:12:34 Z"}
+ assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
+ end
end
def test_failed_json_decoding
@@ -101,5 +99,15 @@ class TestJSONDecoding < ActiveSupport::TestCase
def test_cannot_pass_unsupported_options
assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) }
end
+
+ private
+
+ def with_parse_json_times(value)
+ old_value = ActiveSupport.parse_json_times
+ ActiveSupport.parse_json_times = value
+ yield
+ ensure
+ ActiveSupport.parse_json_times = old_value
+ end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index c4283ee79a..ad358ad21d 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -4,8 +4,11 @@ require 'abstract_unit'
require 'active_support/core_ext/string/inflections'
require 'active_support/json'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TestJSONEncoding < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
class Foo
def initialize(a, b)
@a, @b = a, b
@@ -327,12 +330,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' }}
@@ -342,11 +372,7 @@ 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
@@ -468,31 +494,28 @@ EXPECTED
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)
+ with_time_precision(0) do
+ 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
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))
+ with_time_precision(0) do
+ assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
+ end
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))
+ with_time_precision(0) do
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
+ end
end
- ensure
- ActiveSupport::JSON::Encoding.time_precision = 3
end
def test_twz_to_json_when_wrapping_a_date_time
@@ -507,17 +530,18 @@ EXPECTED
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
end
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ def with_standard_json_time_format(boolean = true)
+ old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
yield
ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ ActiveSupport.use_standard_json_time_format = old
end
- def with_standard_json_time_format(boolean = true)
- old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
+ def with_time_precision(value)
+ old_value = ActiveSupport::JSON::Encoding.time_precision
+ ActiveSupport::JSON::Encoding.time_precision = value
yield
ensure
- ActiveSupport.use_standard_json_time_format = old
+ ActiveSupport::JSON::Encoding.time_precision = old_value
end
end
diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb
index 525082d313..f7e8e9a795 100644
--- a/activesupport/test/key_generator_test.rb
+++ b/activesupport/test/key_generator_test.rb
@@ -29,4 +29,34 @@ class KeyGeneratorTest < ActiveSupport::TestCase
end
end
+class CachingKeyGeneratorTest < ActiveSupport::TestCase
+ def setup
+ @secret = SecureRandom.hex(64)
+ @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2)
+ @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator)
+ end
+
+ test "Generating a cached key for same salt and key size" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ cached_key = @caching_generator.generate_key("some_salt", 32)
+
+ assert_equal derived_key, cached_key
+ assert_equal derived_key.object_id, cached_key.object_id
+ end
+
+ test "Does not cache key for different salt" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ different_salt_key = @caching_generator.generate_key("other_salt", 32)
+
+ assert_not_equal derived_key, different_salt_key
+ end
+
+ test "Does not cache key for different length" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ different_length_key = @caching_generator.generate_key("some_salt", 64)
+
+ assert_not_equal derived_key, different_length_key
+ end
+end
+
end
diff --git a/activesupport/test/multibyte_conformance.rb b/activesupport/test/multibyte_conformance_test.rb
index 2baf724da4..aba81b8248 100644
--- a/activesupport/test/multibyte_conformance.rb
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -10,7 +10,6 @@ require 'tmpdir'
class Downloader
def self.download(from, to)
unless File.exist?(to)
- $stderr.puts "Downloading #{from} to #{to}"
unless File.exist?(File.dirname(to))
system "mkdir -p #{File.dirname(to)}"
end
@@ -20,8 +19,9 @@ class Downloader
target.write l
end
end
- end
- end
+ end
+ end
+ true
end
end
@@ -31,11 +31,15 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd"
UNIDATA_FILE = '/NormalizationTest.txt'
CACHE_DIR = File.join(Dir.tmpdir, 'cache')
+ FileUtils.mkdir_p(CACHE_DIR)
+ RUN_P = begin
+ Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
+ rescue
+ end
def setup
- FileUtils.mkdir_p(CACHE_DIR)
- Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
@proxy = ActiveSupport::Multibyte::Chars
+ skip "Unable to download test data" unless RUN_P
end
def test_normalizations_C
@@ -126,4 +130,4 @@ class MultibyteConformanceTest < ActiveSupport::TestCase
def inspect_codepoints(str)
str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ')
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb
new file mode 100644
index 0000000000..d8ffd7ca9c
--- /dev/null
+++ b/activesupport/test/multibyte_proxy_test.rb
@@ -0,0 +1,34 @@
+# encoding: utf-8
+
+require 'abstract_unit'
+
+class MultibyteProxyText < ActiveSupport::TestCase
+ class AsciiOnlyEncoder
+ attr_reader :wrapped_string
+ alias to_s wrapped_string
+
+ def initialize(string)
+ @wrapped_string = string.gsub(/[^\u0000-\u007F]/, '?')
+ end
+ end
+
+ def with_custom_encoder(encoder)
+ original_proxy_class = ActiveSupport::Multibyte.proxy_class
+
+ begin
+ ActiveSupport::Multibyte.proxy_class = encoder
+
+ yield
+ ensure
+ ActiveSupport::Multibyte.proxy_class = original_proxy_class
+ end
+ end
+
+ test "custom multibyte encoder" do
+ with_custom_encoder(AsciiOnlyEncoder) do
+ assert_equal "s?me string 123", "søme string 123".mb_chars.to_s
+ end
+
+ assert_equal "søme string 123", "søme string 123".mb_chars.to_s
+ end
+end
diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb
index 65aecece71..e6925e9083 100644
--- a/activesupport/test/number_helper_i18n_test.rb
+++ b/activesupport/test/number_helper_i18n_test.rb
@@ -43,6 +43,10 @@ module ActiveSupport
:custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
end
+ def teardown
+ I18n.backend.reload!
+ end
+
def test_number_to_i18n_currency
assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts'))
assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts'))
@@ -50,8 +54,6 @@ module ActiveSupport
end
def test_number_to_currency_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("$10.00", number_to_currency(10, :locale => 'empty'))
assert_equal("-$10.00", number_to_currency(-10, :locale => 'empty'))
end
@@ -80,8 +82,6 @@ module ActiveSupport
end
def test_number_with_i18n_precision_and_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("123456789.123", number_to_rounded(123456789.123456789, :locale => 'empty'))
assert_equal("1.000", number_to_rounded(1.0000, :locale => 'empty'))
end
@@ -92,8 +92,6 @@ module ActiveSupport
end
def test_number_with_i18n_delimiter_and_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("1,000,000.234", number_to_delimited(1000000.234, :locale => 'empty'))
end
@@ -107,8 +105,6 @@ module ActiveSupport
end
def test_number_to_i18n_percentage_and_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("1.000%", number_to_percentage(1, :locale => 'empty'))
assert_equal("1.243%", number_to_percentage(1.2434, :locale => 'empty'))
assert_equal("12434.000%", number_to_percentage(12434, :locale => 'empty'))
@@ -121,8 +117,6 @@ module ActiveSupport
end
def test_number_to_i18n_human_size_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("2 KB", number_to_human_size(2048, :locale => 'empty'))
assert_equal("42 Bytes", number_to_human_size(42, :locale => 'empty'))
end
@@ -142,8 +136,6 @@ module ActiveSupport
end
def test_number_to_human_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal "2 Thousand", number_to_human(2000, :locale => 'empty')
assert_equal "1.23 Billion", number_to_human(1234567890, :locale => 'empty')
end
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index 6d8d835de7..bb51cc68f2 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'active_support/number_helper'
+require 'active_support/core_ext/string/output_safety'
module ActiveSupport
module NumberHelper
@@ -79,6 +80,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
@@ -94,6 +98,7 @@ module ActiveSupport
assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901))
assert_equal("0.78901", number_helper.number_to_delimited(0.78901))
assert_equal("123,456.78", number_helper.number_to_delimited("123456.78"))
+ assert_equal("123,456.78", number_helper.number_to_delimited("123456.78".html_safe))
end
end
@@ -129,6 +134,7 @@ module ActiveSupport
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))
+ assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 4))
end
end
@@ -169,6 +175,7 @@ module ActiveSupport
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 )
+ assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), :precision => 3, :significant => true))
end
end
diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb
index 9d139b61b8..4c0364e68b 100644
--- a/activesupport/test/option_merger_test.rb
+++ b/activesupport/test/option_merger_test.rb
@@ -79,6 +79,15 @@ class OptionMergerTest < ActiveSupport::TestCase
assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new('', '').class
end
+ def test_option_merger_implicit_receiver
+ @options.with_options foo: "bar" do
+ merge! fizz: "buzz"
+ end
+
+ expected = { hello: "world", foo: "bar", fizz: "buzz" }
+ assert_equal expected, @options
+ end
+
private
def method_with_options(options = {})
options
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index 0b54026c64..460a61613e 100644
--- a/activesupport/test/ordered_hash_test.rb
+++ b/activesupport/test/ordered_hash_test.rb
@@ -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/subscriber_test.rb b/activesupport/test/subscriber_test.rb
index 253411aa3d..a88d8d9eba 100644
--- a/activesupport/test/subscriber_test.rb
+++ b/activesupport/test/subscriber_test.rb
@@ -4,20 +4,28 @@ require 'active_support/subscriber'
class TestSubscriber < ActiveSupport::Subscriber
attach_to :doodle
- cattr_reader :event
+ cattr_reader :events
def self.clear
- @@event = nil
+ @@events = []
end
def open_party(event)
- @@event = event
+ events << event
end
private
def private_party(event)
- @@event = event
+ events << event
+ end
+end
+
+# Monkey patch subscriber to test that only one subscriber per method is added.
+class TestSubscriber
+ remove_method :open_party
+ def open_party(event)
+ events << event
end
end
@@ -29,12 +37,18 @@ class SubscriberTest < ActiveSupport::TestCase
def test_attaches_subscribers
ActiveSupport::Notifications.instrument("open_party.doodle")
- assert_equal "open_party.doodle", TestSubscriber.event.name
+ assert_equal "open_party.doodle", TestSubscriber.events.first.name
+ end
+
+ def test_attaches_only_one_subscriber
+ ActiveSupport::Notifications.instrument("open_party.doodle")
+
+ assert_equal 1, TestSubscriber.events.size
end
def test_does_not_attach_private_methods
ActiveSupport::Notifications.instrument("private_party.doodle")
- assert_nil TestSubscriber.event
+ assert_equal [], TestSubscriber.events
end
end
diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb
index 0fa08c0e3a..6f63a8a725 100644
--- a/activesupport/test/test_test.rb
+++ b/activesupport/test/test_test.rb
@@ -28,12 +28,30 @@ class AssertDifferenceTest < ActiveSupport::TestCase
assert_equal 'custom', e.message
end
- def test_assert_no_difference
+ def test_assert_no_difference_pass
assert_no_difference '@object.num' do
# ...
end
end
+ def test_assert_no_difference_fail
+ error = assert_raises(Minitest::Assertion) do
+ assert_no_difference '@object.num' do
+ @object.increment
+ end
+ end
+ assert_equal "\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
+ end
+
+ def test_assert_no_difference_with_message_fail
+ error = assert_raises(Minitest::Assertion) do
+ assert_no_difference '@object.num', 'Object Changed' do
+ @object.increment
+ end
+ end
+ assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
+ end
+
def test_assert_difference
assert_difference '@object.num', +1 do
@object.increment
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index cd79efbe8c..b7a89ed332 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -1,7 +1,10 @@
require 'abstract_unit'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TimeZoneTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_utc_to_local
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500
@@ -124,6 +127,32 @@ class TimeZoneTest < ActiveSupport::TestCase
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
time = ActiveSupport::TimeZone["Hawaii"].local(2007, 2, 5, 15, 30, 45)
assert_equal Time.utc(2007, 2, 5, 15, 30, 45), time.time
@@ -228,6 +257,15 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_equal Time.utc(1999,12,31,19), twz.time
end
+ def test_parse_with_day_omitted
+ with_env_tz 'US/Eastern' do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ assert_equal Time.local(2000, 2, 1), zone.parse('Feb', Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 1), zone.parse('Feb 2005', Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 2), zone.parse('2 Feb 2005', Time.local(2000, 1, 1))
+ end
+ end
+
def test_parse_should_not_black_out_system_timezone_dst_jump
with_env_tz('EET') do
zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
@@ -381,12 +419,4 @@ class TimeZoneTest < ActiveSupport::TestCase
assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"])
assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"])
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb
new file mode 100644
index 0000000000..9632b89d09
--- /dev/null
+++ b/activesupport/test/time_zone_test_helpers.rb
@@ -0,0 +1,16 @@
+module TimeZoneTestHelpers
+ def with_tz_default(tz = nil)
+ old_tz = Time.zone
+ Time.zone = tz
+ yield
+ ensure
+ Time.zone = old_tz
+ 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
+end
diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb
index 753effb54e..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
diff --git a/ci/travis.rb b/ci/travis.rb
index 7e68993332..956a01dbee 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -52,7 +52,7 @@ class Build
def tasks
if activerecord?
- ['mysql:rebuild_databases', "#{adapter}:#{'isolated_' if isolated?}test"]
+ ['db:mysql:rebuild', "#{adapter}:#{'isolated_' if isolated?}test"]
else
["test#{':isolated' if isolated?}"]
end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index 4cfc5b1f10..2770fc73e7 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,13 +1,27 @@
-* Fixed missing line and shadow on service pages(404, 422, 500).
+* Change Posts to Articles in Getting Started sample application in order to
+better align with the actual guides.
- *Dmitry Korotkov*
+ *John Kelly Ferguson*
-* Removed repetitive th tags. Instead of them added one th tag with a colspan attribute.
+* Update all Rails 4.1.0 references to 4.1.1 within the guides and code.
- *Sıtkı Bağdat*
+ *John Kelly Ferguson*
-* Added the Rails maintenance policy to the guides.
+* Split up rows in the Explain Queries table of the ActiveRecord Querying section
+in order to improve readability.
+
+ *John Kelly Ferguson*
+
+* Change all non-HTTP method 'post' references to 'article'.
+
+ *John Kelly Ferguson*
+
+* Updates the maintenance policy to match the latest versions of Rails
*Matias Korhonen*
-Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/guides/CHANGELOG.md) for previous changes.
+* 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/getting_started/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png
index 1918e9bf28..117a78a39f 100644
--- a/guides/assets/images/getting_started/article_with_comments.png
+++ 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 cc12162677..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 e57d4b409e..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
index e263f7f8b2..9f32c68472 100644
--- a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
+++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
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 04ff8b1e2d..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 22f994d993..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
index 89fc0b2605..bd3ae4fa67 100644
--- a/guides/assets/images/getting_started/new_article.png
+++ b/guides/assets/images/getting_started/new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/rails_welcome.jpg b/guides/assets/images/getting_started/rails_welcome.jpg
deleted file mode 100644
index 65a44cdfe5..0000000000
--- a/guides/assets/images/getting_started/rails_welcome.jpg
+++ /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
new file mode 100644
index 0000000000..3e07c948a0
--- /dev/null
+++ 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 ae83b6a68c..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
index 9467df6a07..4dad704f89 100644
--- a/guides/assets/images/getting_started/show_action_for_articles.png
+++ b/guides/assets/images/getting_started/show_action_for_articles.png
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
index ba630cfc23..4e636d09ff 100644
--- a/guides/assets/images/getting_started/template_is_missing_articles_new.png
+++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png
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
index ed89c4f3d7..fd20cd53dc 100644
--- a/guides/assets/images/getting_started/unknown_action_create_for_articles.png
+++ 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_new_for_articles.png b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
index e8f2b9a16a..e948a51e4a 100644
--- a/guides/assets/images/getting_started/unknown_action_new_for_articles.png
+++ b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
Binary files differ
diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js
index e4d25dfb21..a9c7f0d016 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -51,3 +51,9 @@ var guidesIndex = {
window.location = url;
}
};
+
+// Disable autolink inside example code blocks of guides.
+$(document).ready(function() {
+ SyntaxHighlighter.defaults['auto-links'] = false;
+ SyntaxHighlighter.all();
+});
diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css
index 898f9ff05b..318a1ef1c7 100644
--- a/guides/assets/stylesheets/main.css
+++ b/guides/assets/stylesheets/main.css
@@ -381,9 +381,12 @@ a, a:link, a:visited {
font: inherit;
padding-left: .75em;
font-size: .95em;
- background-position: 96% -65px;
+ background-position: 96% 16px;
-webkit-appearance: none;
}
+ .guides-index-small .guides-index-item:hover{
+ background-position: 96% -65px;
+ }
}
#guides {
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
index d44fd9196a..20c64b4a85 100644
--- a/guides/bug_report_templates/action_controller_master.rb
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -2,6 +2,9 @@ unless File.exist?('Gemfile')
File.write('Gemfile', <<-GEMFILE)
source 'https://rubygems.org'
gem 'rails', github: 'rails/rails'
+ gem 'arel', github: 'rails/arel'
+ gem 'rack', github: 'rack/rack'
+ gem 'i18n', github: 'svenfuchs/i18n'
GEMFILE
system 'bundle'
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
index d95354e12d..e7f5d0d5ff 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -3,6 +3,8 @@ unless File.exist?('Gemfile')
source 'https://rubygems.org'
gem 'rails', github: 'rails/rails'
gem 'arel', github: 'rails/arel'
+ gem 'rack', github: 'rack/rack'
+ gem 'i18n', github: 'svenfuchs/i18n'
gem 'sqlite3'
GEMFILE
diff --git a/guides/code/getting_started/.gitignore b/guides/code/getting_started/.gitignore
deleted file mode 100644
index 6a502e997f..0000000000
--- a/guides/code/getting_started/.gitignore
+++ /dev/null
@@ -1,16 +0,0 @@
-# See https://help.github.com/articles/ignoring-files for more about ignoring files.
-#
-# If you find yourself ignoring temporary files generated by your text editor
-# or operating system, you probably want to add a global ignore instead:
-# git config --global core.excludesfile '~/.gitignore_global'
-
-# Ignore bundler config.
-/.bundle
-
-# Ignore the default SQLite database.
-/db/*.sqlite3
-/db/*.sqlite3-journal
-
-# Ignore all logfiles and tempfiles.
-/log/*.log
-/tmp
diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile
deleted file mode 100644
index ecb6e7aa1a..0000000000
--- a/guides/code/getting_started/Gemfile
+++ /dev/null
@@ -1,40 +0,0 @@
-source 'https://rubygems.org'
-
-
-# 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', '~> 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', '~> 4.0.0'
-# See https://github.com/sstephenson/execjs#readme for more supported runtimes
-# 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'
-# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-gem 'jbuilder', '~> 2.0'
-# bundle exec rake doc:rails generates the API under doc/api.
-gem 'sdoc', '~> 0.4.0', group: :doc
-
-# 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-ruby', '~> 3.1.2'
-
-# Use unicorn as the app server
-# gem 'unicorn'
-
-# Use Capistrano for deployment
-# gem 'capistrano-rails', group: :development
-
-# Use debugger
-# gem 'debugger', group: [:development, :test]
-
diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock
deleted file mode 100644
index a2ab76c908..0000000000
--- a/guides/code/getting_started/Gemfile.lock
+++ /dev/null
@@ -1,126 +0,0 @@
-GEM
- remote: https://rubygems.org/
- specs:
- 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.6.3)
- erubis (2.7.0)
- execjs (2.0.2)
- hike (1.2.3)
- i18n (0.6.9)
- jbuilder (2.0.2)
- activesupport (>= 3.0.0)
- multi_json (>= 1.2.0)
- jquery-rails (3.0.4)
- railties (>= 3.0, < 5.0)
- thor (>= 0.14, < 2.0)
- json (1.8.1)
- mail (2.5.4)
- mime-types (~> 1.16)
- treetop (~> 1.4.8)
- mime-types (1.25.1)
- minitest (5.2.1)
- multi_json (1.8.4)
- polyglot (0.3.3)
- rack (1.5.2)
- rack-test (0.6.2)
- rack (>= 1.0)
- 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.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)
- 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.4.1)
- treetop (1.4.15)
- polyglot
- polyglot (>= 0.3.1)
- turbolinks (2.2.0)
- coffee-rails
- tzinfo (1.1.0)
- thread_safe (~> 0.1)
- uglifier (2.4.0)
- execjs (>= 0.3.0)
- json (>= 1.8.0)
-
-PLATFORMS
- ruby
-
-DEPENDENCIES
- coffee-rails (~> 4.0.0)
- jbuilder (~> 2.0)
- jquery-rails
- rails (= 4.1.0)
- sass-rails (~> 4.0.1)
- sdoc (~> 0.4.0)
- spring
- sqlite3
- turbolinks
- uglifier (>= 1.3.0)
diff --git a/guides/code/getting_started/README.rdoc b/guides/code/getting_started/README.rdoc
deleted file mode 100644
index dd4e97e22e..0000000000
--- a/guides/code/getting_started/README.rdoc
+++ /dev/null
@@ -1,28 +0,0 @@
-== README
-
-This README would normally document whatever steps are necessary to get the
-application up and running.
-
-Things you may want to cover:
-
-* Ruby version
-
-* System dependencies
-
-* Configuration
-
-* Database creation
-
-* Database initialization
-
-* How to run the test suite
-
-* Services (job queues, cache servers, search engines, etc.)
-
-* Deployment instructions
-
-* ...
-
-
-Please feel free to use a different markup language if you do not plan to run
-<tt>rake doc:app</tt>.
diff --git a/guides/code/getting_started/Rakefile b/guides/code/getting_started/Rakefile
deleted file mode 100644
index 05de8bb536..0000000000
--- a/guides/code/getting_started/Rakefile
+++ /dev/null
@@ -1,6 +0,0 @@
-# Add your own tasks in files placed in lib/tasks ending in .rake,
-# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-
-require File.expand_path('../config/application', __FILE__)
-
-Blog::Application.load_tasks
diff --git a/guides/code/getting_started/app/assets/javascripts/application.js b/guides/code/getting_started/app/assets/javascripts/application.js
deleted file mode 100644
index 5a4fbaa370..0000000000
--- a/guides/code/getting_started/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// This is a manifest file that'll be compiled into application.js, which will include all the files
-// listed below.
-//
-// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
-// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
-//
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file.
-//
-// stub path allows dependency to be excluded from the asset bundle.
-//
-//= require jquery
-//= require jquery_ujs
-//= require turbolinks
-//= require_tree .
diff --git a/guides/code/getting_started/app/assets/javascripts/comments.js.coffee b/guides/code/getting_started/app/assets/javascripts/comments.js.coffee
deleted file mode 100644
index 24f83d18bb..0000000000
--- a/guides/code/getting_started/app/assets/javascripts/comments.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee b/guides/code/getting_started/app/assets/javascripts/posts.js.coffee
deleted file mode 100644
index 24f83d18bb..0000000000
--- a/guides/code/getting_started/app/assets/javascripts/posts.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee b/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee
deleted file mode 100644
index 24f83d18bb..0000000000
--- a/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/guides/code/getting_started/app/assets/stylesheets/application.css b/guides/code/getting_started/app/assets/stylesheets/application.css
deleted file mode 100644
index 3192ec897b..0000000000
--- a/guides/code/getting_started/app/assets/stylesheets/application.css
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * This is a manifest file that'll be compiled into application.css, which will include all the files
- * listed below.
- *
- * 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.
- *
- *= require_self
- *= require_tree .
- */
diff --git a/guides/code/getting_started/app/assets/stylesheets/comments.css.scss b/guides/code/getting_started/app/assets/stylesheets/comments.css.scss
deleted file mode 100644
index e730912783..0000000000
--- a/guides/code/getting_started/app/assets/stylesheets/comments.css.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// Place all the styles related to the Comments controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss b/guides/code/getting_started/app/assets/stylesheets/posts.css.scss
deleted file mode 100644
index 1a7e15390c..0000000000
--- a/guides/code/getting_started/app/assets/stylesheets/posts.css.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// Place all the styles related to the posts controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss b/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss
deleted file mode 100644
index 77ce11a740..0000000000
--- a/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-// Place all the styles related to the welcome controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/guides/code/getting_started/app/controllers/application_controller.rb b/guides/code/getting_started/app/controllers/application_controller.rb
deleted file mode 100644
index d83690e1b9..0000000000
--- a/guides/code/getting_started/app/controllers/application_controller.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class ApplicationController < ActionController::Base
- # Prevent CSRF attacks by raising an exception.
- # For APIs, you may want to use :null_session instead.
- protect_from_forgery with: :exception
-end
diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb
deleted file mode 100644
index b2d9bcdf7f..0000000000
--- a/guides/code/getting_started/app/controllers/comments_controller.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class CommentsController < ApplicationController
-
- http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
-
- def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(comment_params)
- redirect_to post_path(@post)
- end
-
- def destroy
- @post = Post.find(params[:post_id])
- @comment = @post.comments.find(params[:id])
- @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/concerns/.keep b/guides/code/getting_started/app/controllers/concerns/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/app/controllers/concerns/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb
deleted file mode 100644
index 02689ad67b..0000000000
--- a/guides/code/getting_started/app/controllers/posts_controller.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-class PostsController < ApplicationController
-
- http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
-
- def index
- @posts = Post.all
- end
-
- def show
- @post = Post.find(params[:id])
- end
-
- def edit
- @post = Post.find(params[:id])
- end
-
- def update
- @post = Post.find(params[:id])
-
- if @post.update(post_params)
- redirect_to action: :show, id: @post.id
- else
- render 'edit'
- end
- end
-
- def new
- @post = Post.new
- end
-
- def create
- @post = Post.new(post_params)
-
- if @post.save
- redirect_to action: :show, id: @post.id
- else
- render 'new'
- end
- end
-
- def destroy
- @post = Post.find(params[:id])
- @post.destroy
-
- 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/controllers/welcome_controller.rb b/guides/code/getting_started/app/controllers/welcome_controller.rb
deleted file mode 100644
index f9b859b9c9..0000000000
--- a/guides/code/getting_started/app/controllers/welcome_controller.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class WelcomeController < ApplicationController
- def index
- end
-end
diff --git a/guides/code/getting_started/app/helpers/application_helper.rb b/guides/code/getting_started/app/helpers/application_helper.rb
deleted file mode 100644
index de6be7945c..0000000000
--- a/guides/code/getting_started/app/helpers/application_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ApplicationHelper
-end
diff --git a/guides/code/getting_started/app/helpers/comments_helper.rb b/guides/code/getting_started/app/helpers/comments_helper.rb
deleted file mode 100644
index 0ec9ca5f2d..0000000000
--- a/guides/code/getting_started/app/helpers/comments_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module CommentsHelper
-end
diff --git a/guides/code/getting_started/app/helpers/posts_helper.rb b/guides/code/getting_started/app/helpers/posts_helper.rb
deleted file mode 100644
index a7b8cec898..0000000000
--- a/guides/code/getting_started/app/helpers/posts_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module PostsHelper
-end
diff --git a/guides/code/getting_started/app/helpers/welcome_helper.rb b/guides/code/getting_started/app/helpers/welcome_helper.rb
deleted file mode 100644
index eeead45fc9..0000000000
--- a/guides/code/getting_started/app/helpers/welcome_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module WelcomeHelper
-end
diff --git a/guides/code/getting_started/app/mailers/.keep b/guides/code/getting_started/app/mailers/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/app/mailers/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/app/models/.keep b/guides/code/getting_started/app/models/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/app/models/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/app/models/comment.rb b/guides/code/getting_started/app/models/comment.rb
deleted file mode 100644
index 4e76c5b5b0..0000000000
--- a/guides/code/getting_started/app/models/comment.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class Comment < ActiveRecord::Base
- belongs_to :post
-end
diff --git a/guides/code/getting_started/app/models/concerns/.keep b/guides/code/getting_started/app/models/concerns/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/app/models/concerns/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/app/models/post.rb b/guides/code/getting_started/app/models/post.rb
deleted file mode 100644
index 64e0d721fd..0000000000
--- a/guides/code/getting_started/app/models/post.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Post < ActiveRecord::Base
- has_many :comments, dependent: :destroy
-
- validates :title,
- presence: true,
- length: { minimum: 5 }
-end
diff --git a/guides/code/getting_started/app/views/comments/_comment.html.erb b/guides/code/getting_started/app/views/comments/_comment.html.erb
deleted file mode 100644
index 593493339e..0000000000
--- a/guides/code/getting_started/app/views/comments/_comment.html.erb
+++ /dev/null
@@ -1,15 +0,0 @@
-<p>
- <strong>Commenter:</strong>
- <%= comment.commenter %>
-</p>
-
-<p>
- <strong>Comment:</strong>
- <%= comment.body %>
-</p>
-
-<p>
- <%= link_to 'Destroy Comment', [comment.post, comment],
- method: :delete,
- data: { confirm: 'Are you sure?' } %>
-</p>
diff --git a/guides/code/getting_started/app/views/comments/_form.html.erb b/guides/code/getting_started/app/views/comments/_form.html.erb
deleted file mode 100644
index 00cb3a08f0..0000000000
--- a/guides/code/getting_started/app/views/comments/_form.html.erb
+++ /dev/null
@@ -1,13 +0,0 @@
-<%= form_for([@post, @post.comments.build]) do |f| %>
- <p>
- <%= f.label :commenter %><br />
- <%= f.text_field :commenter %>
- </p>
- <p>
- <%= f.label :body %><br />
- <%= f.text_area :body %>
- </p>
- <p>
- <%= f.submit %>
- </p>
-<% 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
deleted file mode 100644
index d0ba8415e6..0000000000
--- a/guides/code/getting_started/app/views/layouts/application.html.erb
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>Blog</title>
- <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
- <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
- <%= csrf_meta_tags %>
-</head>
-<body>
-
-<%= yield %>
-
-</body>
-</html>
diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/posts/_form.html.erb
deleted file mode 100644
index f2f83585e1..0000000000
--- a/guides/code/getting_started/app/views/posts/_form.html.erb
+++ /dev/null
@@ -1,27 +0,0 @@
-<%= 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>
- <% end %>
- <p>
- <%= f.label :title %><br>
- <%= f.text_field :title %>
- </p>
-
- <p>
- <%= f.label :text %><br>
- <%= f.text_area :text %>
- </p>
-
- <p>
- <%= f.submit %>
- </p>
-<% end %>
-
diff --git a/guides/code/getting_started/app/views/posts/edit.html.erb b/guides/code/getting_started/app/views/posts/edit.html.erb
deleted file mode 100644
index 393e7430d0..0000000000
--- a/guides/code/getting_started/app/views/posts/edit.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1>Edit post</h1>
-
-<%= render 'form' %>
-
-<%= link_to 'Back', action: :index %>
diff --git a/guides/code/getting_started/app/views/posts/index.html.erb b/guides/code/getting_started/app/views/posts/index.html.erb
deleted file mode 100644
index 7369f0396f..0000000000
--- a/guides/code/getting_started/app/views/posts/index.html.erb
+++ /dev/null
@@ -1,21 +0,0 @@
-<h1>Listing Posts</h1>
-<table>
- <tr>
- <th>Title</th>
- <th>Text</th>
- <th></th>
- <th></th>
- <th></th>
- </tr>
-
-<% @posts.each do |post| %>
- <tr>
- <td><%= post.title %></td>
- <td><%= post.text %></td>
- <td><%= link_to 'Show', action: :show, id: post.id %></td>
- <td><%= link_to 'Edit', action: :edit, id: post.id %></td>
- <td><%= link_to 'Destroy', { action: :destroy, id: post.id },
- method: :delete, data: { confirm: 'Are you sure?' } %></td>
- </tr>
-<% end %>
-</table>
diff --git a/guides/code/getting_started/app/views/posts/new.html.erb b/guides/code/getting_started/app/views/posts/new.html.erb
deleted file mode 100644
index efa81038ec..0000000000
--- a/guides/code/getting_started/app/views/posts/new.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1>New post</h1>
-
-<%= render 'form' %>
-
-<%= link_to 'Back', action: :index %>
diff --git a/guides/code/getting_started/app/views/posts/show.html.erb b/guides/code/getting_started/app/views/posts/show.html.erb
deleted file mode 100644
index e99e9edbb3..0000000000
--- a/guides/code/getting_started/app/views/posts/show.html.erb
+++ /dev/null
@@ -1,18 +0,0 @@
-<p>
- <strong>Title:</strong>
- <%= @post.title %>
-</p>
-
-<p>
- <strong>Text:</strong>
- <%= @post.text %>
-</p>
-
-<h2>Comments</h2>
-<%= render @post.comments %>
-
-<h2>Add a comment:</h2>
-<%= render "comments/form" %>
-
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
diff --git a/guides/code/getting_started/app/views/welcome/index.html.erb b/guides/code/getting_started/app/views/welcome/index.html.erb
deleted file mode 100644
index 56be8dd3cc..0000000000
--- a/guides/code/getting_started/app/views/welcome/index.html.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-<h1>Hello, Rails!</h1>
-
-<%= link_to "My Blog", controller: "posts" %>
-<%= link_to "New Post", new_post_path %>
diff --git a/guides/code/getting_started/bin/bundle b/guides/code/getting_started/bin/bundle
deleted file mode 100755
index 45cf37fba4..0000000000
--- a/guides/code/getting_started/bin/bundle
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env ruby
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'rubygems'
-load Gem.bin_path('bundler', 'bundle')
diff --git a/guides/code/getting_started/bin/rails b/guides/code/getting_started/bin/rails
deleted file mode 100755
index 728cd85aa5..0000000000
--- a/guides/code/getting_started/bin/rails
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env ruby
-APP_PATH = File.expand_path('../../config/application', __FILE__)
-require_relative '../config/boot'
-require 'rails/commands'
diff --git a/guides/code/getting_started/bin/rake b/guides/code/getting_started/bin/rake
deleted file mode 100755
index 17240489f6..0000000000
--- a/guides/code/getting_started/bin/rake
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env ruby
-require_relative '../config/boot'
-require 'rake'
-Rake.application.run
diff --git a/guides/code/getting_started/config.ru b/guides/code/getting_started/config.ru
deleted file mode 100644
index ddf869e921..0000000000
--- a/guides/code/getting_started/config.ru
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is used by Rack-based servers to start the application.
-
-require ::File.expand_path('../config/environment', __FILE__)
-run Blog::Application
diff --git a/guides/code/getting_started/config/application.rb b/guides/code/getting_started/config/application.rb
deleted file mode 100644
index 3d7604b659..0000000000
--- a/guides/code/getting_started/config/application.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require File.expand_path('../boot', __FILE__)
-
-require 'rails/all'
-
-# Require the gems listed in Gemfile, including any gems
-# you've limited to :test, :development, or :production.
-Bundler.require(:default, Rails.env)
-
-module Blog
- class Application < Rails::Application
- # Settings in config/environments/* take precedence over those specified here.
- # Application configuration should go into files in config/initializers
- # -- all .rb files in that directory are automatically loaded.
-
- # Custom directories with classes and modules you want to be autoloadable.
- # config.autoload_paths += %W(#{config.root}/extras)
- end
-end
diff --git a/guides/code/getting_started/config/boot.rb b/guides/code/getting_started/config/boot.rb
deleted file mode 100644
index 5e5f0c1fac..0000000000
--- a/guides/code/getting_started/config/boot.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-
-require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/guides/code/getting_started/config/database.yml b/guides/code/getting_started/config/database.yml
deleted file mode 100644
index 51a4dd459d..0000000000
--- a/guides/code/getting_started/config/database.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# SQLite version 3.x
-# gem install sqlite3
-#
-# Ensure the SQLite 3 gem is defined in your Gemfile
-# gem 'sqlite3'
-development:
- adapter: sqlite3
- database: db/development.sqlite3
- pool: 5
- timeout: 5000
-
-# 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
- database: db/test.sqlite3
- pool: 5
- timeout: 5000
-
-production:
- adapter: sqlite3
- database: db/production.sqlite3
- pool: 5
- timeout: 5000
diff --git a/guides/code/getting_started/config/environment.rb b/guides/code/getting_started/config/environment.rb
deleted file mode 100644
index e7e341c960..0000000000
--- a/guides/code/getting_started/config/environment.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Load the Rails application.
-require File.expand_path('../application', __FILE__)
-
-# Initialize the Rails application.
-Blog::Application.initialize!
diff --git a/guides/code/getting_started/config/environments/development.rb b/guides/code/getting_started/config/environments/development.rb
deleted file mode 100644
index 7e5692b08b..0000000000
--- a/guides/code/getting_started/config/environments/development.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-Blog::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
- # every request. This slows down response time but is perfect for development
- # since you don't have to restart the web server when you make code changes.
- config.cache_classes = false
-
- # Do not eager load code on boot.
- config.eager_load = false
-
- # Show full error reports and disable caching.
- config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
-
- # Don't care if the mailer can't send.
- config.action_mailer.raise_delivery_errors = false
-
- # Print deprecation notices to the Rails logger.
- config.active_support.deprecation = :log
-
- # Only use best-standards-support built into browsers.
- config.action_dispatch.best_standards_support = :builtin
-
- # Raise an error on page load if there are pending migrations.
- config.active_record.migration_error = :page_load
-
- # Debug mode disables concatenation and preprocessing of assets.
- config.assets.debug = true
-end
diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb
deleted file mode 100644
index 8c514e065e..0000000000
--- a/guides/code/getting_started/config/environments/production.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-Blog::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 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
-
- # Full error reports are disabled and caching is turned on.
- config.consider_all_requests_local = false
- config.action_controller.perform_caching = true
-
- # Enable Rack::Cache to put a simple HTTP cache in front of your application
- # Add `rack-cache` to your Gemfile before enabling this.
- # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
- # config.action_dispatch.rack_cache = true
-
- # Disable Rails's static asset server (Apache or nginx will already do this).
- config.serve_static_assets = false
-
- # Compress JavaScripts and CSS.
- config.assets.js_compressor = :uglifier
- # config.assets.css_compressor = :sass
-
- # Whether to fallback to assets pipeline if a precompiled asset is missed.
- config.assets.compile = false
-
- # 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'
-
- # Specifies the header that your server uses for sending files.
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
-
- # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
- # config.force_ssl = true
-
- # Set to :debug to see everything in the log.
- config.log_level = :info
-
- # Prepend all log lines with the following tags.
- # config.log_tags = [ :subdomain, :uuid ]
-
- # Use a different logger for distributed setups.
- # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
-
- # Use a different cache store in production.
- # config.cache_store = :mem_cache_store
-
- # Enable serving of images, stylesheets, and JavaScripts from an asset server.
- # config.action_controller.asset_host = "http://assets.example.com"
-
- # 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 )
-
- # 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 cannot be found).
- config.i18n.fallbacks = true
-
- # Send deprecation notices to registered listeners.
- config.active_support.deprecation = :notify
-
- # Disable automatic flushing of the log to improve performance.
- # config.autoflush_log = false
-
- # Use default logging formatter so that PID and timestamp are not suppressed.
- config.log_formatter = ::Logger::Formatter.new
-end
diff --git a/guides/code/getting_started/config/environments/test.rb b/guides/code/getting_started/config/environments/test.rb
deleted file mode 100644
index 34ab1530d1..0000000000
--- a/guides/code/getting_started/config/environments/test.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-Blog::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
- # test suite. You never need to work with it otherwise. Remember that
- # your test database is "scratch space" for the test suite and is wiped
- # and recreated between test runs. Don't rely on the data there!
- config.cache_classes = true
-
- # Do not eager load code on boot. This avoids loading your whole application
- # just for the purpose of running a single test. If you are using a tool that
- # preloads Rails for running tests, you may have to set it to true.
- config.eager_load = false
-
- # Configure static asset server for tests with Cache-Control for performance.
- config.serve_static_assets = true
- config.static_cache_control = 'public, max-age=3600'
-
- # Show full error reports and disable caching.
- config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
-
- # Raise exceptions instead of rendering exception templates.
- config.action_dispatch.show_exceptions = false
-
- # Disable request forgery protection in test environment.
- config.action_controller.allow_forgery_protection = false
-
- # Tell Action Mailer not to deliver emails to the real world.
- # The :test delivery method accumulates sent emails in the
- # ActionMailer::Base.deliveries array.
- config.action_mailer.delivery_method = :test
-
- # Print deprecation notices to the stderr.
- config.active_support.deprecation = :stderr
-end
diff --git a/guides/code/getting_started/config/initializers/backtrace_silencers.rb b/guides/code/getting_started/config/initializers/backtrace_silencers.rb
deleted file mode 100644
index 59385cdf37..0000000000
--- a/guides/code/getting_started/config/initializers/backtrace_silencers.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
-
-# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-# Rails.backtrace_cleaner.remove_silencers!
diff --git a/guides/code/getting_started/config/initializers/filter_parameter_logging.rb b/guides/code/getting_started/config/initializers/filter_parameter_logging.rb
deleted file mode 100644
index 4a994e1e7b..0000000000
--- a/guides/code/getting_started/config/initializers/filter_parameter_logging.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Configure sensitive parameters which will be filtered from the log file.
-Rails.application.config.filter_parameters += [:password]
diff --git a/guides/code/getting_started/config/initializers/inflections.rb b/guides/code/getting_started/config/initializers/inflections.rb
deleted file mode 100644
index ac033bf9dc..0000000000
--- a/guides/code/getting_started/config/initializers/inflections.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Add new inflection rules using the following format. Inflections
-# are locale specific, and you may define rules for as many different
-# locales as you wish. All of these examples are active by default:
-# ActiveSupport::Inflector.inflections(:en) do |inflect|
-# inflect.plural /^(ox)$/i, '\1en'
-# inflect.singular /^(ox)en/i, '\1'
-# inflect.irregular 'person', 'people'
-# inflect.uncountable %w( fish sheep )
-# end
-
-# These inflection rules are supported but not enabled by default:
-# ActiveSupport::Inflector.inflections(:en) do |inflect|
-# inflect.acronym 'RESTful'
-# end
diff --git a/guides/code/getting_started/config/initializers/locale.rb b/guides/code/getting_started/config/initializers/locale.rb
deleted file mode 100644
index d89dac7c6a..0000000000
--- a/guides/code/getting_started/config/initializers/locale.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
-# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
-# Rails.application.config.time_zone = 'Central Time (US & Canada)'
-
-# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
-# Rails.application.config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
-# Rails.application.config.i18n.default_locale = :de
diff --git a/guides/code/getting_started/config/initializers/mime_types.rb b/guides/code/getting_started/config/initializers/mime_types.rb
deleted file mode 100644
index 72aca7e441..0000000000
--- a/guides/code/getting_started/config/initializers/mime_types.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# 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/guides/code/getting_started/config/initializers/secret_token.rb b/guides/code/getting_started/config/initializers/secret_token.rb
deleted file mode 100644
index aaf57731be..0000000000
--- a/guides/code/getting_started/config/initializers/secret_token.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Your secret key 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.
-# You can use `rake secret` to generate a secure secret key.
-
-# Make sure your secret_key_base is kept private
-# if you're sharing your code publicly.
-Blog::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
deleted file mode 100644
index 3b2ca93ab9..0000000000
--- a/guides/code/getting_started/config/initializers/session_store.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-Blog::Application.config.session_store :cookie_store, key: '_blog_session'
diff --git a/guides/code/getting_started/config/initializers/wrap_parameters.rb b/guides/code/getting_started/config/initializers/wrap_parameters.rb
deleted file mode 100644
index 33725e95fd..0000000000
--- a/guides/code/getting_started/config/initializers/wrap_parameters.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# This file contains settings for ActionController::ParamsWrapper which
-# is enabled by default.
-
-# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
-ActiveSupport.on_load(:action_controller) do
- wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
-end
-
-# To enable root element in JSON for ActiveRecord objects.
-# ActiveSupport.on_load(:active_record) do
-# self.include_root_in_json = true
-# end
diff --git a/guides/code/getting_started/config/locales/en.yml b/guides/code/getting_started/config/locales/en.yml
deleted file mode 100644
index 0653957166..0000000000
--- a/guides/code/getting_started/config/locales/en.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# Files in the config/locales directory are used for internationalization
-# and are automatically loaded by Rails. If you want to use locales other
-# than English, add the necessary files in this directory.
-#
-# To use the locales, use `I18n.t`:
-#
-# I18n.t 'hello'
-#
-# In views, this is aliased to just `t`:
-#
-# <%= t('hello') %>
-#
-# To use a different locale, set it with `I18n.locale`:
-#
-# I18n.locale = :es
-#
-# This would use the information in config/locales/es.yml.
-#
-# To learn more, please read the Rails Internationalization guide
-# available at http://guides.rubyonrails.org/i18n.html.
-
-en:
- hello: "Hello world"
diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb
deleted file mode 100644
index 0155b613a3..0000000000
--- a/guides/code/getting_started/config/routes.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-Blog::Application.routes.draw do
- resources :posts do
- resources :comments
- end
-
- root "welcome#index"
-end
diff --git a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb b/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb
deleted file mode 100644
index 602bef31ab..0000000000
--- a/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class CreatePosts < ActiveRecord::Migration
- def change
- create_table :posts do |t|
- t.string :title
- t.text :text
-
- t.timestamps
- end
- end
-end
diff --git a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb
deleted file mode 100644
index 3e51f9c0f7..0000000000
--- a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CreateComments < ActiveRecord::Migration
- def change
- create_table :comments do |t|
- t.string :commenter
- t.text :body
- t.references :post, index: true
-
- t.timestamps
- end
- end
-end
diff --git a/guides/code/getting_started/db/schema.rb b/guides/code/getting_started/db/schema.rb
deleted file mode 100644
index 101fe712a1..0000000000
--- a/guides/code/getting_started/db/schema.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# encoding: UTF-8
-# This file is auto-generated from the current state of the database. Instead
-# of editing this file, please use the migrations feature of Active Record to
-# incrementally modify your database, and then regenerate this schema definition.
-#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
-#
-# It's strongly recommended that you check this file into your version control system.
-
-ActiveRecord::Schema.define(version: 20130122045842) do
-
- create_table "comments", force: true do |t|
- t.string "commenter"
- t.text "body"
- t.integer "post_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "comments", ["post_id"], name: "index_comments_on_post_id"
-
- create_table "posts", force: true do |t|
- t.string "title"
- t.text "text"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
-end
diff --git a/guides/code/getting_started/db/seeds.rb b/guides/code/getting_started/db/seeds.rb
deleted file mode 100644
index 4edb1e857e..0000000000
--- a/guides/code/getting_started/db/seeds.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file should contain all the record creation needed to seed the database with its default values.
-# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
-#
-# Examples:
-#
-# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
-# Mayor.create(name: 'Emanuel', city: cities.first)
diff --git a/guides/code/getting_started/lib/assets/.keep b/guides/code/getting_started/lib/assets/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/lib/assets/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/lib/tasks/.keep b/guides/code/getting_started/lib/tasks/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/lib/tasks/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/log/.keep b/guides/code/getting_started/log/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/log/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/public/404.html b/guides/code/getting_started/public/404.html
deleted file mode 100644
index 3265cc8e33..0000000000
--- a/guides/code/getting_started/public/404.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>The page you were looking for doesn't exist (404)</title>
- <style>
- body {
- background-color: #EFEFEF;
- color: #2E2F30;
- text-align: center;
- font-family: arial, sans-serif;
- }
-
- div.dialog {
- width: 25em;
- margin: 4em auto 0 auto;
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #BBB;
- border-top: #B00100 solid 4px;
- border-top-left-radius: 9px;
- 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 {
- font-size: 100%;
- color: #730E15;
- line-height: 1.5em;
- }
-
- body > p {
- width: 33em;
- margin: 0 auto 1em;
- padding: 1em 0;
- 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);
- }
- </style>
-</head>
-
-<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>
- <p>If you are the application owner check the logs for more information.</p>
-</body>
-</html>
diff --git a/guides/code/getting_started/public/422.html b/guides/code/getting_started/public/422.html
deleted file mode 100644
index d823a8fc77..0000000000
--- a/guides/code/getting_started/public/422.html
+++ /dev/null
@@ -1,60 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>The change you wanted was rejected (422)</title>
- <style>
- body {
- background-color: #EFEFEF;
- color: #2E2F30;
- text-align: center;
- font-family: arial, sans-serif;
- }
-
- div.dialog {
- width: 25em;
- margin: 4em auto 0 auto;
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #BBB;
- border-top: #B00100 solid 4px;
- border-top-left-radius: 9px;
- 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 {
- font-size: 100%;
- color: #730E15;
- line-height: 1.5em;
- }
-
- body > p {
- width: 33em;
- margin: 0 auto 1em;
- padding: 1em 0;
- 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);
- }
- </style>
-</head>
-
-<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>
- <p>If you are the application owner check the logs for more information.</p>
-</body>
-</html>
diff --git a/guides/code/getting_started/public/500.html b/guides/code/getting_started/public/500.html
deleted file mode 100644
index ebf6d4c00c..0000000000
--- a/guides/code/getting_started/public/500.html
+++ /dev/null
@@ -1,59 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>We're sorry, but something went wrong (500)</title>
- <style>
- body {
- background-color: #EFEFEF;
- color: #2E2F30;
- text-align: center;
- font-family: arial, sans-serif;
- }
-
- div.dialog {
- width: 25em;
- margin: 4em auto 0 auto;
- border: 1px solid #CCC;
- border-right-color: #999;
- border-left-color: #999;
- border-bottom-color: #BBB;
- border-top: #B00100 solid 4px;
- border-top-left-radius: 9px;
- 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 {
- font-size: 100%;
- color: #730E15;
- line-height: 1.5em;
- }
-
- body > p {
- width: 33em;
- margin: 0 auto 1em;
- padding: 1em 0;
- 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);
- }
- </style>
-</head>
-
-<body>
- <!-- This file lives in public/500.html -->
- <div class="dialog">
- <h1>We're sorry, but something went wrong.</h1>
- </div>
- <p>If you are the application owner check the logs for more information.</p>
-</body>
-</html>
diff --git a/guides/code/getting_started/public/favicon.ico b/guides/code/getting_started/public/favicon.ico
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/public/favicon.ico
+++ /dev/null
diff --git a/guides/code/getting_started/public/robots.txt b/guides/code/getting_started/public/robots.txt
deleted file mode 100644
index 3c9c7c01f3..0000000000
--- a/guides/code/getting_started/public/robots.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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: *
-# Disallow: /
diff --git a/guides/code/getting_started/test/controllers/.keep b/guides/code/getting_started/test/controllers/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/controllers/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/test/controllers/comments_controller_test.rb b/guides/code/getting_started/test/controllers/comments_controller_test.rb
deleted file mode 100644
index 2ec71b4ec5..0000000000
--- a/guides/code/getting_started/test/controllers/comments_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class CommentsControllerTest < ActionController::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/controllers/posts_controller_test.rb b/guides/code/getting_started/test/controllers/posts_controller_test.rb
deleted file mode 100644
index 7a6ee4f1db..0000000000
--- a/guides/code/getting_started/test/controllers/posts_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class PostsControllerTest < ActionController::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/controllers/welcome_controller_test.rb b/guides/code/getting_started/test/controllers/welcome_controller_test.rb
deleted file mode 100644
index dff8e9d2c5..0000000000
--- a/guides/code/getting_started/test/controllers/welcome_controller_test.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'test_helper'
-
-class WelcomeControllerTest < ActionController::TestCase
- test "should get index" do
- get :index
- assert_response :success
- end
-
-end
diff --git a/guides/code/getting_started/test/fixtures/.keep b/guides/code/getting_started/test/fixtures/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/fixtures/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml
deleted file mode 100644
index 9e409d8a61..0000000000
--- a/guides/code/getting_started/test/fixtures/comments.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-
-one:
- commenter: MyString
- body: MyText
- post_id:
-
-two:
- commenter: MyString
- body: MyText
- post_id:
diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/posts.yml
deleted file mode 100644
index 46b01c3bb4..0000000000
--- a/guides/code/getting_started/test/fixtures/posts.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-
-one:
- title: MyString
- text: MyText
-
-two:
- title: MyString
- text: MyText
diff --git a/guides/code/getting_started/test/helpers/.keep b/guides/code/getting_started/test/helpers/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/helpers/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/test/helpers/comments_helper_test.rb b/guides/code/getting_started/test/helpers/comments_helper_test.rb
deleted file mode 100644
index 2518c16bd5..0000000000
--- a/guides/code/getting_started/test/helpers/comments_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class CommentsHelperTest < ActionView::TestCase
-end
diff --git a/guides/code/getting_started/test/helpers/posts_helper_test.rb b/guides/code/getting_started/test/helpers/posts_helper_test.rb
deleted file mode 100644
index 48549c2ea1..0000000000
--- a/guides/code/getting_started/test/helpers/posts_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class PostsHelperTest < ActionView::TestCase
-end
diff --git a/guides/code/getting_started/test/helpers/welcome_helper_test.rb b/guides/code/getting_started/test/helpers/welcome_helper_test.rb
deleted file mode 100644
index d6ded5995f..0000000000
--- a/guides/code/getting_started/test/helpers/welcome_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class WelcomeHelperTest < ActionView::TestCase
-end
diff --git a/guides/code/getting_started/test/integration/.keep b/guides/code/getting_started/test/integration/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/integration/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/test/mailers/.keep b/guides/code/getting_started/test/mailers/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/mailers/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/test/models/.keep b/guides/code/getting_started/test/models/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/models/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/test/models/comment_test.rb b/guides/code/getting_started/test/models/comment_test.rb
deleted file mode 100644
index b6d6131a96..0000000000
--- a/guides/code/getting_started/test/models/comment_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class CommentTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/models/post_test.rb b/guides/code/getting_started/test/models/post_test.rb
deleted file mode 100644
index 6d9d463a71..0000000000
--- a/guides/code/getting_started/test/models/post_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class PostTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/test_helper.rb b/guides/code/getting_started/test/test_helper.rb
deleted file mode 100644
index f91a4375dc..0000000000
--- a/guides/code/getting_started/test/test_helper.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-ENV["RAILS_ENV"] = "test"
-require File.expand_path('../../config/environment', __FILE__)
-require 'rails/test_help'
-
-class ActiveSupport::TestCase
- 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
- # -- they do not yet inherit this setting
- fixtures :all
-
- # Add more helper methods to be used by all tests here...
-end
diff --git a/guides/code/getting_started/vendor/assets/javascripts/.keep b/guides/code/getting_started/vendor/assets/javascripts/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/vendor/assets/javascripts/.keep
+++ /dev/null
diff --git a/guides/code/getting_started/vendor/assets/stylesheets/.keep b/guides/code/getting_started/vendor/assets/stylesheets/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/vendor/assets/stylesheets/.keep
+++ /dev/null
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index 9d488a8a15..9d1d5567f6 100644
--- a/guides/rails_guides.rb
+++ b/guides/rails_guides.rb
@@ -24,11 +24,11 @@ begin
require 'redcarpet'
rescue LoadError
# This can happen if doc:guides is executed in an application.
- $stderr.puts('Generating guides requires Redcarpet 2.1.1+.')
+ $stderr.puts('Generating guides requires Redcarpet 3.1.2+.')
$stderr.puts(<<ERROR) if bundler?
Please add
- gem 'redcarpet', '~> 2.1.1'
+ gem 'redcarpet', '~> 3.1.2'
to the Gemfile, run
diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb
index 169453400f..a78c2e9fca 100644
--- a/guides/rails_guides/helpers.rb
+++ b/guides/rails_guides/helpers.rb
@@ -39,7 +39,7 @@ module RailsGuides
def author(name, nick, image = 'credits_pic_blank.gif', &block)
image = "images/#{image}"
- result = content_tag(:img, nil, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91)
+ result = tag(:img, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91)
result << content_tag(:h3, name)
result << content_tag(:p, capture(&block))
content_tag(:div, result, :class => 'clearfix', :id => nick)
diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb
index 489aa3ea7a..8a908a4339 100644
--- a/guides/rails_guides/levenshtein.rb
+++ b/guides/rails_guides/levenshtein.rb
@@ -1,31 +1,39 @@
module RailsGuides
module Levenshtein
- # Based on the pseudocode in http://en.wikipedia.org/wiki/Levenshtein_distance
- def self.distance(s1, s2)
- s = s1.unpack('U*')
- t = s2.unpack('U*')
- m = s.length
- n = t.length
+ # This code is based directly on the Text gem implementation
+ # Returns a value representing the "cost" of transforming str1 into str2
+ def self.distance str1, str2
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+ max = n/2
- # matrix initialization
- d = []
- 0.upto(m) { |i| d << [i] }
- 0.upto(n) { |j| d[0][j] = j }
+ return m if (0 == n)
+ return n if (0 == m)
+ return n if (n - m).abs > max
- # distance computation
- 1.upto(m) do |i|
- 1.upto(n) do |j|
- cost = s[i] == t[j] ? 0 : 1
- d[i][j] = [
- d[i-1][j] + 1, # deletion
- d[i][j-1] + 1, # insertion
- d[i-1][j-1] + cost, # substitution
- ].min
+ d = (0..m).to_a
+ x = nil
+
+ str1.each_char.each_with_index do |char1,i|
+ e = i+1
+
+ str2.each_char.each_with_index do |char2,j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j+1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
end
+
+ d[m] = x
end
- # all done
- return d[m][n]
+ return x
end
end
end
diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb
index 547c6d2c15..1ea18ba9f5 100644
--- a/guides/rails_guides/markdown.rb
+++ b/guides/rails_guides/markdown.rb
@@ -47,8 +47,7 @@ module RailsGuides
end
def dom_id_text(text)
- text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/[^a-z0-9]+/, ' ')
- .strip.gsub(/\s+/, '-')
+ text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/\s+/, '-')
end
def engine
@@ -79,10 +78,10 @@ module RailsGuides
def generate_structure
@headings_for_index = []
if @body.present?
- @body = Nokogiri::HTML(@body).tap do |doc|
+ @body = Nokogiri::HTML.fragment(@body).tap do |doc|
hierarchy = []
- doc.at('body').children.each do |node|
+ doc.children.each do |node|
if node.name =~ /^h[3-6]$/
case node.name
when 'h3'
@@ -116,7 +115,7 @@ module RailsGuides
end
end
- @index = Nokogiri::HTML(engine.render(raw_index)).tap do |doc|
+ @index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc|
doc.at('ol')[:class] = 'chapters'
end.to_html
@@ -130,8 +129,8 @@ module RailsGuides
end
def generate_title
- if heading = Nokogiri::HTML(@header).at(:h2)
- @title = "#{heading.text} — Ruby on Rails Guides".html_safe
+ if heading = Nokogiri::HTML.fragment(@header).at(:h2)
+ @title = "#{heading.text} — Ruby on Rails Guides"
else
@title = "Ruby on Rails Guides"
end
diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md
index 8c633fa169..52eeb4c2bc 100644
--- a/guides/source/2_3_release_notes.md
+++ b/guides/source/2_3_release_notes.md
@@ -8,7 +8,7 @@ Rails 2.3 delivers a variety of new and improved features, including pervasive R
Application Architecture
------------------------
-There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.rubyforge.org/) modular web server interface, and renewed support for Rails Engines.
+There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.github.io/) modular web server interface, and renewed support for Rails Engines.
### Rack Integration
@@ -594,7 +594,7 @@ The internals of the various <code>rake gem</code> tasks have been substantially
* Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`).
* Rails Guides have been converted from AsciiDoc to Textile markup.
* Scaffolded views and controllers have been cleaned up a bit.
-* `script/server` now accepts a <tt>--path</tt> argument to mount a Rails application from a specific path.
+* `script/server` now accepts a `--path` argument to mount a Rails application from a specific path.
* If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing.
* Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files.
@@ -618,4 +618,4 @@ A few pieces of older code are deprecated in this release:
Credits
-------
-Release notes compiled by [Mike Gunderloy](http://afreshcup.com.) This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3.
+Release notes compiled by [Mike Gunderloy](http://afreshcup.com). This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3.
diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md
index dd81ec58f9..aec3a383d6 100644
--- a/guides/source/3_0_release_notes.md
+++ b/guides/source/3_0_release_notes.md
@@ -294,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/)
@@ -308,7 +308,7 @@ More Information:
Major re-write was done in the Action View helpers, implementing Unobtrusive JavaScript (UJS) hooks and removing the old inline AJAX commands. This enables Rails to use any compliant UJS driver to implement the UJS hooks in the helpers.
-What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper.) To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example:
+What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example:
```ruby
form_for @post, :remote => true
@@ -521,7 +521,7 @@ A large effort was made in Active Support to make it cherry pickable, that is, y
These are the main changes in Active Support:
* Large clean up of the library removing unused methods throughout.
-* Active Support no longer provides vendored versions of [TZInfo](http://tzinfo.rubyforge.org/), [Memcache Client](http://deveiate.org/projects/RMemCache/) and [Builder](http://builder.rubyforge.org/,) these are all included as dependencies and installed via the `bundle install` command.
+* Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the `bundle install` command.
* Safe buffers are implemented in `ActiveSupport::SafeBuffer`.
* Added `Array.uniq_by` and `Array.uniq_by!`.
* Removed `Array#rand` and backported `Array#sample` from Ruby 1.9.
@@ -608,4 +608,4 @@ Credits
See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails 3. Kudos to all of them.
-Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net.)
+Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net).
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 485f8c756b..7626296e7d 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -173,7 +173,7 @@ The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/spr
### HTTP Streaming
-HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of nginx and unicorn is ready to take advantage of it.
+HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it.
### Default JS library is now jQuery
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index ce811a583b..2416e1a228 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -187,7 +187,7 @@ Action Pack
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`.
@@ -562,4 +562,4 @@ 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.
-Rails 3.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.)
+Rails 3.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev).
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
index 8fcfc71351..5f4bdaaa8f 100644
--- a/guides/source/4_1_release_notes.md
+++ b/guides/source/4_1_release_notes.md
@@ -157,7 +157,7 @@ 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)
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails)
for a detailed write up.
### Active Record enums
@@ -291,6 +291,10 @@ for detailed changes.
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
-----------
@@ -346,8 +350,16 @@ for detailed changes.
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))
+* 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
-------------
@@ -384,7 +396,7 @@ for detailed changes.
* Removed deprecated `scope` use without passing a callable object.
* Removed deprecated `transaction_joinable=` in favor of `begin_transaction`
- with `d:joinable` option.
+ with a `:joinable` option.
* Removed deprecated `decrement_open_transactions`.
@@ -453,6 +465,12 @@ for detailed changes.
### 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))
@@ -533,12 +551,15 @@ for detailed changes.
* 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))
+* Enable partial indexes for `sqlite >= 3.8.0`.
+ ([Pull Request](https://github.com/rails/rails/pull/13350))
* Make `change_column_null`
- revertable. ([Commit](https://github.com/rails/rails/commit/724509a9d5322ff502aefa90dd282ba33a281a96))
+ 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
------------
@@ -656,9 +677,8 @@ for detailed changes.
[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 time difference by stubbing
- `Time.now` and
- `Date.today`. ([Pull Request](https://github.com/rails/rails/pull/12824))
+ 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`
@@ -691,12 +711,15 @@ for detailed changes.
* 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/commit/8e21ae37ad9fef6b7393a84f9b5f2e18a831e49a))
+ list. ([Pull Request](https://github.com/rails/rails/pull/13341))
-* Introduce Module#concerning: a natural, low-ceremony way to separate
+* 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
-------
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
new file mode 100644
index 0000000000..e8d1dc361a
--- /dev/null
+++ b/guides/source/4_2_release_notes.md
@@ -0,0 +1,402 @@
+Ruby on Rails 4.2 Release Notes
+===============================
+
+Highlights in Rails 4.2:
+
+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.2
+----------------------
+
+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.1 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 4.2. 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-1-to-rails-4-2)
+guide.
+
+
+Major Features
+--------------
+
+### Foreign key support
+
+The migration DSL now supports adding and removing foreign keys. They are dumped
+to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql`
+adapters support foreign keys.
+
+```ruby
+# add a foreign key to `articles.author_id` referencing `authors.id`
+add_foreign_key :articles, :authors
+
+# add a foreign key to `articles.author_id` referencing `users.lng_id`
+add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+
+# remove the foreign key on `accounts.branch_id`
+remove_foreign_key :accounts, :branches
+
+# remove the foreign key on `accounts.owner_id`
+remove_foreign_key :accounts, column: :owner_id
+```
+
+See the API documentation on
+[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key)
+and
+[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key)
+for a full description.
+
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Removals
+
+* The `rails application` command has been removed without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/11616))
+
+### Deprecations
+
+* Deprecated `Rails::Rack::LogTailer` without replacement.
+ ([Commit](https://github.com/rails/rails/commit/84a13e019e93efaa8994b3f8303d635a7702dbce))
+
+### Notable changes
+
+* Introduced `--skip-gems` option in the app generator to skip gems such as
+ `turbolinks` and `coffee-rails` that does not have their own specific flags.
+ ([Commit](https://github.com/rails/rails/commit/10565895805887d4faf004a6f71219da177f78b7))
+
+* Introduced `bin/setup` script to bootstrap an application.
+ ([Pull Request](https://github.com/rails/rails/pull/15189))
+
+* Changed default value for `config.assets.digest` to `true` in development.
+ ([Pull Request](https://github.com/rails/rails/pull/15155))
+
+* Introduced an API to register new extensions for `rake notes`.
+ ([Pull Request](https://github.com/rails/rails/pull/14379))
+
+* Introduced `Rails.gem_version` as a convenience method to return `Gem::Version.new(Rails.version)`.
+ ([Pull Request](https://github.com/rails/rails/pull/14101))
+
+* Introduced an `after_bundle` callback in the Rails templates.
+ ([Pull Request](https://github.com/rails/rails/pull/16359))
+
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
+ in favor of `AbstractController::Helpers::MissingHelperError`.
+ ([Commit](https://github.com/rails/rails/commit/a1ddde15ae0d612ff2973de9cf768ed701b594e8))
+
+### Deprecations
+
+* Deprecated support for setting the `:to` option of a router to a symbol or a
+ string that does not contain a `#` character:
+
+ ```ruby
+ get '/posts', to: MyRackApp => (No change necessary)
+ get '/posts', to: 'post#index' => (No change necessary)
+ get '/posts', to: 'posts' => get '/posts', controller: :posts
+ get '/posts', to: :index => get '/posts', action: :index
+ ```
+
+ ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9))
+
+### Notable changes
+
+* `render nothing: true` or rendering a `nil` body no longer add a single
+ space padding to the response body.
+ ([Pull Request](https://github.com/rails/rails/pull/14883))
+
+* Introduced the `always_permitted_parameters` option to configure which
+ parameters are permitted globally. The default value of this configuration
+ is `['controller', 'action']`.
+ ([Pull Request](https://github.com/rails/rails/pull/15933))
+
+* The `*_filter` family methods has been removed from the documentation. Their
+ usage are discouraged in favor of the `*_action` family methods:
+
+ ```
+ after_filter => after_action
+ append_after_filter => append_after_action
+ append_around_filter => append_around_action
+ append_before_filter => append_before_action
+ around_filter => around_action
+ before_filter => before_action
+ prepend_after_filter => prepend_after_action
+ prepend_around_filter => prepend_around_action
+ prepend_before_filter => prepend_before_action
+ skip_after_filter => skip_after_action
+ skip_around_filter => skip_around_action
+ skip_before_filter => skip_before_action
+ skip_filter => skip_action_callback
+ ```
+
+ If your application is depending on these methods, you should use the
+ replacement `*_action` methods instead. These methods will be deprecated in
+ the future and eventually removed from Rails.
+
+ (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de),
+ [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4))
+
+* Added HTTP method `MKCALENDAR` from RFC-4791
+ ([Pull Request](https://github.com/rails/rails/pull/15121))
+
+* `*_fragment.action_controller` notifications now include the controller and action name
+ in the payload.
+ ([Pull Request](https://github.com/rails/rails/pull/14137))
+
+* Segments that are passed into URL helpers are now automatically escaped.
+ ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f))
+
+* Improved Routing Error page with fuzzy matching for route search.
+ ([Pull Request](https://github.com/rails/rails/pull/14619))
+
+* Added option to disable logging of CSRF failures.
+ ([Pull Request](https://github.com/rails/rails/pull/14280))
+
+
+Action View
+-------------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Deprecations
+
+* Deprecated `AbstractController::Base.parent_prefixes`.
+ Override `AbstractController::Base.local_prefixes` when you want to change
+ where to find views.
+ ([Pull Request](https://github.com/rails/rails/pull/15026))
+
+* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`,
+ arguments should be passed as a hash instead.
+ ([Pull Request](https://github.com/rails/rails/pull/14243))
+
+### Notable changes
+
+* The form helpers no longer generate a `<div>` element with inline CSS around
+ the hidden fields.
+ ([Pull Request](https://github.com/rails/rails/pull/14738))
+
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Notable changes
+
+* Added the `show_previews` configuration option for enabling mailer previews
+ outside of the development environment.
+ ([Pull Request](https://github.com/rails/rails/pull/15970))
+
+
+Active Record
+-------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed `cache_attributes` and friends. All attributes are cached.
+ ([Pull Request](https://github.com/rails/rails/pull/15429))
+
+* Removed deprecated method `ActiveRecord::Base.quoted_locking_column`.
+ ([Pull Request](https://github.com/rails/rails/pull/15612))
+
+* Removed deprecated `ActiveRecord::Migrator.proper_table_name`. Use the
+ `proper_table_name` instance method on `ActiveRecord::Migration` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/15512))
+
+* Removed unused `:timestamp` type. Transparently alias it to `:datetime`
+ in all cases. Fixes inconsistencies when column types are sent outside of
+ `ActiveRecord`, such as for XML Serialization.
+ ([Pull Request](https://github.com/rails/rails/pull/15184))
+
+### Deprecations
+
+* Deprecated broken support for automatic detection of counter caches on
+ `has_many :through` associations. You should instead manually specify the
+ counter cache on the `has_many` and `belongs_to` associations for the
+ through records.
+ ([Pull Request](https://github.com/rails/rails/pull/15754))
+
+* Deprecated `serialized_attributes` without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/15704))
+
+* Deprecated returning `nil` from `column_for_attribute` when no column
+ exists. It will return a null object in Rails 5.0
+ ([Pull Request](https://github.com/rails/rails/pull/15878))
+
+* Deprecated using `.joins`, `.preload` and `.eager_load` with associations
+ that depends on the instance state (i.e. those defined with a scope that
+ takes an argument) without replacement.
+ ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1))
+
+* Deprecated passing Active Record objects to `.find` or `.exists?`. Call
+ `#id` on the objects first.
+ (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270),
+ [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7))
+
+* Deprecated 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.
+
+ 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 `ArgumentError` for ranges
+ with excluding beginnings.
+
+ ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3))
+
+### Notable changes
+
+* Added a `:required` option to singular associations, which defines a
+ presence validation on the association.
+ ([Pull Request](https://github.com/rails/rails/pull/16056))
+
+* Introduced `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the
+ record is invalid.
+ ([Pull Request](https://github.com/rails/rails/pull/8639))
+
+* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`,
+ meaning that it no longer retains the extra attributes from custom
+ `select`s.
+ ([Pull Request](https://github.com/rails/rails/pull/15866))
+
+* Introduced the `bin/rake db:purge` task to empty the database for the
+ current environment.
+ ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d))
+
+* `ActiveRecord::Dirty` now detects in-place changes to mutable values.
+ Serialized attributes on ActiveRecord models will no longer save when
+ unchanged. This also works with other types such as string columns and json
+ columns on PostgreSQL.
+ (Pull Requests [1](https://github.com/rails/rails/pull/15674),
+ [2](https://github.com/rails/rails/pull/15786),
+ [3](https://github.com/rails/rails/pull/15788))
+
+* Added support for `#pretty_print` in `ActiveRecord::Base` objects.
+ ([Pull Request](https://github.com/rails/rails/pull/15172))
+
+* PostgreSQL and SQLite adapters no longer add a default limit of 255
+ characters on string columns.
+ ([Pull Request](https://github.com/rails/rails/pull/14579))
+
+* `sqlite3:///some/path` now resolves to the absolute system path
+ `/some/path`. For relative paths, use `sqlite3:some/path` instead.
+ (Previously, `sqlite3:///some/path` resolved to the relative path
+ `some/path`. This behaviour was deprecated on Rails 4.1.)
+ ([Pull Request](https://github.com/rails/rails/pull/14569))
+
+* Introduced `#validate` as an alias for `#valid?`.
+ ([Pull Request](https://github.com/rails/rails/pull/14456))
+
+* `#touch` now accepts multiple attributes to be touched at once.
+ ([Pull Request](https://github.com/rails/rails/pull/14423))
+
+* Added support for fractional seconds for MySQL 5.6 and above.
+ (Pull Request [1](https://github.com/rails/rails/pull/8240),
+ [2](https://github.com/rails/rails/pull/14359))
+
+* Added support for the `citext` column type in PostgreSQL adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/12523))
+
+* Added support for user-created range types in PostgreSQL adapter.
+ ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032))
+
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Removals
+
+* Removed deprecated `Validator#setup` without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/15617))
+
+### Notable changes
+
+* Introduced `undo_changes` method in `ActiveModel::Dirty` to restore the
+ changed (dirty) attributes to their previous values.
+ ([Pull Request](https://github.com/rails/rails/pull/14861))
+
+* `has_secure_password` now verifies that the given password is less than 72
+ characters if validations are enabled.
+ ([Pull Request](https://github.com/rails/rails/pull/15708))
+
+* Introduced `#validate` as an alias for `#valid?`.
+ ([Pull Request](https://github.com/rails/rails/pull/14456))
+
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+* Removed deprecated `Numeric#ago`, `Numeric#until`, `Numeric#since`,
+ `Numeric#from_now`.
+ ([Commit](https://github.com/rails/rails/commit/f1eddea1e3f6faf93581c43651348f48b2b7d8bb))
+
+* Removed deprecated string based terminators for `ActiveSupport::Callbacks`.
+ ([Pull Request](https://github.com/rails/rails/pull/15100))
+
+### Deprecations
+
+* Deprecated `Class#superclass_delegating_accessor`, use
+ `Class#class_attribute` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/14271))
+
+* Deprecated `ActiveSupport::SafeBuffer#prepend!` as
+ `ActiveSupport::SafeBuffer#prepend` now performs the same function.
+ ([Pull Request](https://github.com/rails/rails/pull/14529))
+
+### Notable changes
+
+* Added `Hash#transform_values` and `Hash#transform_values!` to simplify a
+ common pattern where the values of a hash must change, but the keys are left
+ the same.
+ ([Pull Request](https://github.com/rails/rails/pull/15819))
+
+* The `humanize` inflector helper now strips any leading underscores.
+ ([Commit](https://github.com/rails/rails/commit/daaa21bc7d20f2e4ff451637423a25ff2d5e75c7))
+
+* Introduce `Concern#class_methods` as an alternative to
+ `module ClassMethods`, as well as `Kernel#concern` to avoid the
+ `module Foo; extend ActiveSupport::Concern; end` boilerplate.
+ ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad))
+
+
+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 today. Kudos to all of them.
+
+[railties]: https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md
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 27c53689c4..f84f1cb376 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.2 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.16/">http://guides.rubyonrails.org/v3.2.16/</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.4/">Rails 4.1.4</a>, <a href="http://guides.rubyonrails.org/v4.0.8/">Rails 4.0.8</a>, <a href="http://guides.rubyonrails.org/v3.2.19/">Rails 3.2.19</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 222d86afe9..4c04a06dbb 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
@@ -260,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.
@@ -364,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.
@@ -604,6 +619,30 @@ It is also possible to pass a custom serializer that responds to `load` and
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
---------------------------
@@ -1039,7 +1078,7 @@ Rails keeps a log file for each environment in the `log` folder. These are extre
### Parameters Filtering
-You can filter certain request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log.
+You can filter out sensitive request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log.
```ruby
config.filter_parameters << :password
@@ -1047,7 +1086,7 @@ config.filter_parameters << :password
### Redirects Filtering
-Sometimes it's desirable to filter out from log files some sensible locations your application is redirecting to.
+Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to.
You can do that by using the `config.filter_redirect` configuration option:
```ruby
@@ -1088,7 +1127,7 @@ class ApplicationController < ActionController::Base
private
def record_not_found
- render text: "404 Not Found", status: 404
+ render plain: "404 Not Found", status: 404
end
end
```
@@ -1125,8 +1164,65 @@ class ClientsController < ApplicationController
end
```
+WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). If you would like to dynamically generate error pages, see [Custom errors page](#custom-errors-page).
+
NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information.
+
+### Custom errors page
+
+You can customize the layout of your error handling using controllers and views.
+First define your app own routes to display the errors page.
+
+* `config/application.rb`
+
+ ```ruby
+ config.exceptions_app = self.routes
+ ```
+
+* `config/routes.rb`
+
+ ```ruby
+ get '/404', to: 'errors#not_found'
+ get '/422', to: 'errors#unprocessable_entity'
+ get '/500', to: 'errors#server_error'
+ ```
+
+Create the controller and views.
+
+* `app/controllers/errors_controller.rb`
+
+ ```ruby
+ class ErrorsController < ActionController::Base
+ layout 'error'
+
+ def not_found
+ render status: :not_found
+ end
+
+ def unprocessable_entity
+ render status: :unprocessable_entity
+ end
+
+ def server_error
+ render status: :server_error
+ end
+ end
+ ```
+
+* `app/views`
+
+ ```
+ errors/
+ not_found.html.erb
+ unprocessable_entity.html.erb
+ server_error.html.erb
+ layouts/
+ error.html.erb
+ ```
+
+Do not forget to set the correct status code on the controller as shown before. You should avoid using the database or any complex operations because the user is already on the error page. Generating another error while on an error page could cause issues.
+
Force HTTPS protocol
--------------------
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index 6dc7fb1606..9ad9319255 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -17,7 +17,10 @@ After reading this guide, you will know:
Introduction
------------
-Action Mailer allows you to send emails from your application using mailer classes and views. Mailers work very similarly to controllers. They inherit from `ActionMailer::Base` and live in `app/mailers`, and they have associated views that appear in `app/views`.
+Action Mailer allows you to send emails from your application using mailer classes
+and views. Mailers work very similarly to controllers. They inherit from
+`ActionMailer::Base` and live in `app/mailers`, and they have associated views
+that appear in `app/views`.
Sending Emails
--------------
@@ -30,7 +33,7 @@ views.
#### Create the Mailer
```bash
-$ rails generate mailer UserMailer
+$ bin/rails generate mailer UserMailer
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
@@ -84,8 +87,11 @@ Here is a quick explanation of the items presented in the preceding method. For
a full list of all available options, please have a look further down at the
Complete List of Action Mailer user-settable attributes section.
-* `default Hash` - This is a hash of default values for any email you send from this mailer. In this case we are setting the `:from` header to a value for all messages in this class. This can be overridden on a per-email basis.
-* `mail` - The actual email message, we are passing the `:to` and `:subject` headers in.
+* `default Hash` - This is a hash of default values for any email you send from
+this mailer. In this case we are setting the `:from` header to a value for all
+messages in this class. This can be overridden on a per-email basis.
+* `mail` - The actual email message, we are passing the `:to` and `:subject`
+headers in.
Just like controllers, any instance variables we define in the method become
available for use in the views.
@@ -146,12 +152,12 @@ Setting this up is painfully simple.
First, let's create a simple `User` scaffold:
```bash
-$ rails generate scaffold user name email login
-$ rake db:migrate
+$ bin/rails generate scaffold user name email login
+$ bin/rake db:migrate
```
Now that we have a user model to play with, we will just edit the
-`app/controllers/users_controller.rb` make it instruct the UserMailer to deliver
+`app/controllers/users_controller.rb` make it instruct the `UserMailer` to deliver
an email to the newly created user by editing the create action and inserting a
call to `UserMailer.welcome_email` right after the user is successfully saved:
@@ -230,9 +236,11 @@ different, encode your content and pass in the encoded content and encoding in a
```ruby
encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
- attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
- encoding: 'SpecialEncoding',
- content: encoded_content }
+ attachments['filename.jpg'] = {
+ mime_type: 'application/x-gzip',
+ encoding: 'SpecialEncoding',
+ content: encoded_content
+ }
```
NOTE: If you specify an encoding, Mail will assume that your content is already
@@ -301,7 +309,7 @@ email address in the format `"Full Name <email>"`.
```ruby
def welcome_email(user)
@user = user
- email_with_name = "#{@user.name} <#{@user.email}>"
+ email_with_name = %("#{@user.name}" <#{@user.email}>)
mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
end
```
@@ -406,6 +414,22 @@ globally in `config/application.rb`:
config.action_mailer.default_url_options = { host: 'example.com' }
```
+Because of this behavior you cannot use any of the `*_path` helpers inside of
+an email. Instead you will need to use the associated `*_url` helper. For example
+instead of using
+
+```
+<%= link_to 'welcome', welcome_path %>
+```
+
+You will need to use:
+
+```
+<%= link_to 'welcome', welcome_url %>
+```
+
+By using the full URL, your links will now work in your emails.
+
#### generating URLs with `url_for`
You need to pass the `only_path: false` option when using `url_for`. This will
@@ -608,7 +632,7 @@ files (environment.rb, production.rb, etc...)
| Configuration | Description |
|---------------|-------------|
|`logger`|Generates 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.|
-|`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>|
+|`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:<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.|
@@ -617,7 +641,7 @@ files (environment.rb, production.rb, etc...)
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
For a complete writeup of possible configurations see the
-[Action Mailer section](configuring.html#configuring-action-mailer) in
+[Configuring Action Mailer](configuring.html#configuring-action-mailer) in
our Configuring Rails Applications guide.
### Example Action Mailer Configuration
@@ -662,6 +686,7 @@ You can find detailed instructions on how to test your mailers in the
Intercepting Emails
-------------------
+
There are situations where you need to edit an email before it's
delivered. Fortunately Action Mailer provides hooks to intercept every
email. You can register an interceptor to make modifications to mail messages
@@ -685,5 +710,5 @@ ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.st
NOTE: The example above uses a custom environment called "staging" for a
production like server but for testing purposes. You can read
-[Creating Rails environments](./configuring.html#creating-rails-environments)
+[Creating Rails environments](configuring.html#creating-rails-environments)
for more information about custom Rails environments.
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index 6a355a5177..ef7ef5a50e 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -28,22 +28,22 @@ For each controller there is an associated directory in the `app/views` director
Let's take a look at what Rails does by default when creating a new resource using the scaffold generator:
```bash
-$ rails generate scaffold post
+$ bin/rails generate scaffold article
[...]
invoke scaffold_controller
- create app/controllers/posts_controller.rb
+ create app/controllers/articles_controller.rb
invoke erb
- create app/views/posts
- create app/views/posts/index.html.erb
- create app/views/posts/edit.html.erb
- create app/views/posts/show.html.erb
- create app/views/posts/new.html.erb
- create app/views/posts/_form.html.erb
+ create app/views/articles
+ create app/views/articles/index.html.erb
+ create app/views/articles/edit.html.erb
+ create app/views/articles/show.html.erb
+ create app/views/articles/new.html.erb
+ create app/views/articles/_form.html.erb
[...]
```
There is a naming convention for views in Rails. Typically, the views share their name with the associated controller action, as you can see above.
-For example, the index controller action of the `posts_controller.rb` will use the `index.html.erb` view file in the `app/views/posts` directory.
+For example, the index controller action of the `articles_controller.rb` will use the `index.html.erb` view file in the `app/views/articles` directory.
The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Later on this guide you can find a more detailed documentation of each one of these three components.
@@ -276,23 +276,23 @@ Partial Layouts
Partials can have their own layouts applied to them. These layouts are different than the ones that are specified globally for the entire action, but they work in a similar fashion.
-Let's say we're displaying a post on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Post`:
+Let's say we're displaying an article on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Article`:
```ruby
-Post.create(body: 'Partial Layouts are cool!')
+Article.create(body: 'Partial Layouts are cool!')
```
-In the `show` template, we'll render the `_post` partial wrapped in the `box` layout:
+In the `show` template, we'll render the `_article` partial wrapped in the `box` layout:
-**posts/show.html.erb**
+**articles/show.html.erb**
```erb
-<%= render partial: 'post', layout: 'box', locals: {post: @post} %>
+<%= render partial: 'article', layout: 'box', locals: {article: @article} %>
```
-The `box` layout simply wraps the `_post` partial in a `div`:
+The `box` layout simply wraps the `_article` partial in a `div`:
-**posts/_box.html.erb**
+**articles/_box.html.erb**
```html+erb
<div class='box'>
@@ -300,13 +300,13 @@ The `box` layout simply wraps the `_post` partial in a `div`:
</div>
```
-The `_post` partial wraps the post's `body` in a `div` with the `id` of the post using the `div_for` helper:
+The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper:
-**posts/_post.html.erb**
+**articles/_article.html.erb**
```html+erb
-<%= div_for(post) do %>
- <p><%= post.body %></p>
+<%= div_for(article) do %>
+ <p><%= article.body %></p>
<% end %>
```
@@ -314,22 +314,22 @@ this would output the following:
```html
<div class='box'>
- <div id='post_1'>
+ <div id='article_1'>
<p>Partial Layouts are cool!</p>
</div>
</div>
```
-Note that the partial layout has access to the local `post` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix.
+Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix.
-You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_post` partial, we could do this instead:
+You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead:
-**posts/show.html.erb**
+**articles/show.html.erb**
```html+erb
-<% render(layout: 'box', locals: {post: @post}) do %>
- <%= div_for(post) do %>
- <p><%= post.body %></p>
+<% render(layout: 'box', locals: {article: @article}) do %>
+ <%= div_for(article) do %>
+ <p><%= article.body %></p>
<% end %>
<% end %>
```
@@ -356,18 +356,18 @@ This module provides methods for generating container tags, such as `div`, for y
Renders a container tag that relates to your Active Record Object.
-For example, given `@post` is the object of `Post` class, you can do:
+For example, given `@article` is the object of `Article` class, you can do:
```html+erb
-<%= content_tag_for(:tr, @post) do %>
- <td><%= @post.title %></td>
+<%= content_tag_for(:tr, @article) do %>
+ <td><%= @article.title %></td>
<% end %>
```
This will generate this HTML output:
```html
-<tr id="post_1234" class="post">
+<tr id="article_1234" class="article">
<td>Hello World!</td>
</tr>
```
@@ -375,34 +375,34 @@ This will generate this HTML output:
You can also supply HTML attributes as an additional option hash. For example:
```html+erb
-<%= content_tag_for(:tr, @post, class: "frontpage") do %>
- <td><%= @post.title %></td>
+<%= content_tag_for(:tr, @article, class: "frontpage") do %>
+ <td><%= @article.title %></td>
<% end %>
```
Will generate this HTML output:
```html
-<tr id="post_1234" class="post frontpage">
+<tr id="article_1234" class="article frontpage">
<td>Hello World!</td>
</tr>
```
-You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@posts` is an array of two `Post` objects:
+You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@articles` is an array of two `Article` objects:
```html+erb
-<%= content_tag_for(:tr, @posts) do |post| %>
- <td><%= post.title %></td>
+<%= content_tag_for(:tr, @articles) do |article| %>
+ <td><%= article.title %></td>
<% end %>
```
Will generate this HTML output:
```html
-<tr id="post_1234" class="post">
+<tr id="article_1234" class="article">
<td>Hello World!</td>
</tr>
-<tr id="post_1235" class="post">
+<tr id="article_1235" class="article">
<td>Ruby on Rails Rocks!</td>
</tr>
```
@@ -412,15 +412,15 @@ Will generate this HTML output:
This is actually a convenient method which calls `content_tag_for` internally with `:div` as the tag name. You can pass either an Active Record object or a collection of objects. For example:
```html+erb
-<%= div_for(@post, class: "frontpage") do %>
- <td><%= @post.title %></td>
+<%= div_for(@article, class: "frontpage") do %>
+ <td><%= @article.title %></td>
<% end %>
```
Will generate this HTML output:
```html
-<div id="post_1234" class="post frontpage">
+<div id="article_1234" class="article frontpage">
<td>Hello World!</td>
</div>
```
@@ -590,14 +590,14 @@ This helper makes building an Atom feed easy. Here's a full usage example:
**config/routes.rb**
```ruby
-resources :posts
+resources :articles
```
-**app/controllers/posts_controller.rb**
+**app/controllers/articles_controller.rb**
```ruby
def index
- @posts = Post.all
+ @articles = Article.all
respond_to do |format|
format.html
@@ -606,20 +606,20 @@ def index
end
```
-**app/views/posts/index.atom.builder**
+**app/views/articles/index.atom.builder**
```ruby
atom_feed do |feed|
- feed.title("Posts Index")
- feed.updated((@posts.first.created_at))
+ feed.title("Articles Index")
+ feed.updated((@articles.first.created_at))
- @posts.each do |post|
- feed.entry(post) do |entry|
- entry.title(post.title)
- entry.content(post.body, type: 'html')
+ @articles.each do |article|
+ feed.entry(article) do |entry|
+ entry.title(article.title)
+ entry.content(article.body, type: 'html')
entry.author do |author|
- author.name(post.author_name)
+ author.name(article.author_name)
end
end
end
@@ -697,7 +697,7 @@ For example, let's say we have a standard application layout, but also a special
</html>
```
-**app/views/posts/special.html.erb**
+**app/views/articles/special.html.erb**
```html+erb
<p>This is a special page.</p>
@@ -714,7 +714,7 @@ For example, let's say we have a standard application layout, but also a special
Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute.
```ruby
-date_select("post", "published_on")
+date_select("article", "published_on")
```
#### datetime_select
@@ -722,7 +722,7 @@ date_select("post", "published_on")
Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based attribute.
```ruby
-datetime_select("post", "published_on")
+datetime_select("article", "published_on")
```
#### distance_of_time_in_words
@@ -904,10 +904,10 @@ The params hash has a nested person value, which can therefore be accessed with
Returns a checkbox tag tailored for accessing a specified attribute.
```ruby
-# Let's say that @post.validated? is 1:
-check_box("post", "validated")
-# => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
-# <input name="post[validated]" type="hidden" value="0" />
+# Let's say that @article.validated? is 1:
+check_box("article", "validated")
+# => <input type="checkbox" id="article_validated" name="article[validated]" value="1" />
+# <input name="article[validated]" type="hidden" value="0" />
```
#### fields_for
@@ -939,7 +939,7 @@ file_field(:user, :avatar)
Creates a form and a scope around a specific model object that is used as a base for questioning about values for the fields.
```html+erb
-<%= form_for @post do |f| %>
+<%= form_for @article do |f| %>
<%= f.label :title, 'Title' %>:
<%= f.text_field :title %><br>
<%= f.label :body, 'Body' %>:
@@ -961,8 +961,8 @@ hidden_field(:user, :token)
Returns a label tag tailored for labelling an input field for a specified attribute.
```ruby
-label(:post, :title)
-# => <label for="post_title">Title</label>
+label(:article, :title)
+# => <label for="article_title">Title</label>
```
#### password_field
@@ -979,11 +979,11 @@ password_field(:login, :pass)
Returns a radio button tag for accessing a specified attribute.
```ruby
-# Let's say that @post.category returns "rails":
-radio_button("post", "category", "rails")
-radio_button("post", "category", "java")
-# => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
-# <input type="radio" id="post_category_java" name="post[category]" value="java" />
+# Let's say that @article.category returns "rails":
+radio_button("article", "category", "rails")
+radio_button("article", "category", "java")
+# => <input type="radio" id="article_category_rails" name="article[category]" value="rails" checked="checked" />
+# <input type="radio" id="article_category_java" name="article[category]" value="java" />
```
#### text_area
@@ -1002,8 +1002,8 @@ text_area(:comment, :text, size: "20x30")
Returns an input tag of the "text" type tailored for accessing a specified attribute.
```ruby
-text_field(:post, :title)
-# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" />
+text_field(:article, :title)
+# => <input type="text" id="article_title" name="article[title]" value="#{@article.title}" />
```
#### email_field
@@ -1035,28 +1035,28 @@ Returns `select` and `option` tags for the collection of existing return values
Example object structure for use with this method:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
- has_many :posts
+ has_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
end
end
```
-Sample usage (selecting the associated Author for an instance of Post, `@post`):
+Sample usage (selecting the associated Author for an instance of Article, `@article`):
```ruby
-collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true})
+collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true})
```
-If `@post.author_id` is 1, this would return:
+If `@article.author_id` is 1, this would return:
```html
-<select name="post[author_id]">
+<select name="article[author_id]">
<option value="">Please select</option>
<option value="1" selected="selected">D. Heinemeier Hansson</option>
<option value="2">D. Thomas</option>
@@ -1071,33 +1071,33 @@ Returns `radio_button` tags for the collection of existing return values of `met
Example object structure for use with this method:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
- has_many :posts
+ has_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
end
end
```
-Sample usage (selecting the associated Author for an instance of Post, `@post`):
+Sample usage (selecting the associated Author for an instance of Article, `@article`):
```ruby
-collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
+collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial)
```
-If `@post.author_id` is 1, this would return:
+If `@article.author_id` is 1, this would return:
```html
-<input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
-<label for="post_author_id_1">D. Heinemeier Hansson</label>
-<input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
-<label for="post_author_id_2">D. Thomas</label>
-<input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
-<label for="post_author_id_3">M. Clark</label>
+<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" checked="checked" />
+<label for="article_author_id_1">D. Heinemeier Hansson</label>
+<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" />
+<label for="article_author_id_2">D. Thomas</label>
+<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" />
+<label for="article_author_id_3">M. Clark</label>
```
#### collection_check_boxes
@@ -1107,34 +1107,34 @@ Returns `check_box` tags for the collection of existing return values of `method
Example object structure for use with this method:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_and_belongs_to_many :authors
end
class Author < ActiveRecord::Base
- has_and_belongs_to_many :posts
+ has_and_belongs_to_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
end
end
```
-Sample usage (selecting the associated Authors for an instance of Post, `@post`):
+Sample usage (selecting the associated Authors for an instance of Article, `@article`):
```ruby
-collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
+collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial)
```
-If `@post.author_ids` is [1], this would return:
+If `@article.author_ids` is [1], this would return:
```html
-<input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
-<label for="post_author_ids_1">D. Heinemeier Hansson</label>
-<input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
-<label for="post_author_ids_2">D. Thomas</label>
-<input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
-<label for="post_author_ids_3">M. Clark</label>
-<input name="post[author_ids][]" type="hidden" value="" />
+<input id="article_author_ids_1" name="article[author_ids][]" type="checkbox" value="1" checked="checked" />
+<label for="article_author_ids_1">D. Heinemeier Hansson</label>
+<input id="article_author_ids_2" name="article[author_ids][]" type="checkbox" value="2" />
+<label for="article_author_ids_2">D. Thomas</label>
+<input id="article_author_ids_3" name="article[author_ids][]" type="checkbox" value="3" />
+<label for="article_author_ids_3">M. Clark</label>
+<input name="article[author_ids][]" type="hidden" value="" />
```
#### country_options_for_select
@@ -1222,13 +1222,13 @@ Create a select tag and a series of contained option tags for the provided objec
Example:
```ruby
-select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true})
+select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true})
```
-If `@post.person_id` is 1, this would become:
+If `@article.person_id` is 1, this would become:
```html
-<select name="post[person_id]">
+<select name="article[person_id]">
<option value=""></option>
<option value="1" selected="selected">David</option>
<option value="2">Sam</option>
@@ -1303,10 +1303,10 @@ file_field_tag 'attachment'
Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`.
```html+erb
-<%= form_tag '/posts' do %>
+<%= form_tag '/articles' do %>
<div><%= submit_tag 'Save' %></div>
<% end %>
-# => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
+# => <form action="/articles" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
```
#### hidden_field_tag
@@ -1368,8 +1368,8 @@ select_tag "people", "<option>David</option>"
Creates a submit button with the text provided as the caption.
```ruby
-submit_tag "Publish this post"
-# => <input name="commit" type="submit" value="Publish this post" />
+submit_tag "Publish this article"
+# => <input name="commit" type="submit" value="Publish this article" />
```
#### text_area_tag
@@ -1377,8 +1377,8 @@ submit_tag "Publish this post"
Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
```ruby
-text_area_tag 'post'
-# => <textarea id="post" name="post"></textarea>
+text_area_tag 'article'
+# => <textarea id="article" name="article"></textarea>
```
#### text_field_tag
@@ -1550,7 +1550,7 @@ end
Sanitizes a block of CSS code.
-#### strip_links(html)
+#### strip_links(html)
Strips all link tags from text leaving just the link text.
```ruby
@@ -1568,9 +1568,9 @@ strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
# => Blog: Visit.
```
-#### strip_tags(html)
+#### strip_tags(html)
-Strips all HTML tags from the html, including comments.
+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
@@ -1585,13 +1585,24 @@ strip_tags("<b>Bold</b> no more! <a href='more.html'>See more</a>")
NB: The output may still contain unescaped '<', '>', '&' characters and confuse browsers.
+### CsrfHelper
+
+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 %>
+```
+
+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).
Localized Views
---------------
Action View has the ability render different templates depending on the current locale.
-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.
+For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/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.
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.
@@ -1605,6 +1616,6 @@ def set_expert_locale
end
```
-Then you could create special views like `app/views/posts/show.expert.html.erb` that would only be displayed to expert users.
+Then you could create special views like `app/views/articles/show.expert.html.erb` that would only be displayed to expert users.
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 0019d08328..3eaeeff389 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -198,3 +198,26 @@ person.valid? # => true
person.token = nil
person.valid? # => raises ActiveModel::StrictValidationFailed
```
+
+### ActiveModel::Naming
+
+Naming adds a number of class methods which make the naming and routing
+easier to manage. The module defines the `model_name` class method which
+will define a number of accessors using some `ActiveSupport::Inflector` methods.
+
+```ruby
+class Person
+ extend ActiveModel::Naming
+end
+
+Person.model_name.name # => "Person"
+Person.model_name.singular # => "person"
+Person.model_name.plural # => "people"
+Person.model_name.element # => "person"
+Person.model_name.human # => "Person"
+Person.model_name.collection # => "people"
+Person.model_name.param_key # => "person"
+Person.model_name.i18n_key # => :person
+Person.model_name.route_key # => "people"
+Person.model_name.singular_route_key # => "person"
+```
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index a184f0753d..eff93ce41d 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -82,13 +82,13 @@ by underscores. Examples:
* Model Class - Singular with the first letter of each word capitalized (e.g.,
`BookClub`).
-| Model / Class | Table / Schema |
-| ------------- | -------------- |
-| `Post` | `posts` |
-| `LineItem` | `line_items` |
-| `Deer` | `deers` |
-| `Mouse` | `mice` |
-| `Person` | `people` |
+| Model / Class | Table / Schema |
+| ---------------- | -------------- |
+| `Article` | `articles` |
+| `LineItem` | `line_items` |
+| `Deer` | `deers` |
+| `Mouse` | `mice` |
+| `Person` | `people` |
### Schema Conventions
@@ -120,9 +120,9 @@ to Active Record instances:
* `(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
- associations. For example, a `comments_count` column in a `Post` class that
+ associations. For example, a `comments_count` column in a `Articles` class that
has many instances of `Comment` will cache the number of existent comments
- for each post.
+ for each article.
NOTE: While these column names are optional, they are in fact reserved by Active Record. Steer clear of reserved keywords unless you want the extra functionality. For example, `type` is a reserved keyword used to designate a table using Single Table Inheritance (STI). If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling.
@@ -309,11 +309,11 @@ into the database. There are several methods that you can use to check your
models and validate that an attribute value is not empty, is unique and not
already in the database, follows a specific format and many more.
-Validation is a very important issue to consider when persisting to database, so
-the methods `create`, `save` and `update` take it into account when
+Validation is a very important issue to consider when persisting to the database, so
+the methods `save` and `update` take it into account when
running: they return `false` when validation fails and they didn't actually
-perform any operation on database. All of these have a bang counterpart (that
-is, `create!`, `save!` and `update!`), which are stricter in that
+perform any operation on the database. All of these have a bang counterpart (that
+is, `save!` and `update!`), which are stricter in that
they raise the exception `ActiveRecord::RecordInvalid` if validation fails.
A quick example to illustrate:
@@ -322,8 +322,9 @@ class User < ActiveRecord::Base
validates :name, presence: true
end
-User.create # => false
-User.create! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+user = User.new
+user.save # => false
+user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
```
You can learn more about validations in the [Active Record Validations
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 667433285f..9c7e60cbb0 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -15,7 +15,7 @@ After reading this guide, you will know:
The Object Life Cycle
---------------------
-During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object life cycle</em> so that you can control your application and its data.
+During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this *object life cycle* so that you can control your application and its data.
Callbacks allow you to trigger logic before or after an alteration of an object's state.
@@ -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.
@@ -258,27 +261,27 @@ WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by
Relational Callbacks
--------------------
-Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many posts. A user's posts should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Post` model:
+Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model:
```ruby
class User < ActiveRecord::Base
- has_many :posts, dependent: :destroy
+ has_many :articles, dependent: :destroy
end
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
after_destroy :log_destroy_action
def log_destroy_action
- puts 'Post destroyed'
+ puts 'Article destroyed'
end
end
>> user = User.first
=> #<User id: 1>
->> user.posts.create!
-=> #<Post id: 1, user_id: 1>
+>> user.articles.create!
+=> #<Article id: 1, user_id: 1>
>> user.destroy
-Post destroyed
+Article destroyed
=> #<User id: 1>
```
@@ -325,7 +328,7 @@ When writing conditional callbacks, it is possible to mix both `:if` and `:unles
```ruby
class Comment < ActiveRecord::Base
after_create :send_email_to_author, if: :author_wants_emails?,
- unless: Proc.new { |comment| comment.post.ignore_comments? }
+ unless: Proc.new { |comment| comment.article.ignore_comments? }
end
```
diff --git a/guides/source/migrations.md b/guides/source/active_record_migrations.md
index 64c4e1e07e..229c6ee458 100644
--- a/guides/source/migrations.md
+++ b/guides/source/active_record_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
@@ -120,7 +121,7 @@ Of course, calculating timestamps is no fun, so Active Record provides a
generator to handle making it for you:
```bash
-$ rails generate migration AddPartNumberToProducts
+$ bin/rails generate migration AddPartNumberToProducts
```
This will create an empty but appropriately named migration:
@@ -137,7 +138,7 @@ followed by a list of column names and types then a migration containing the
appropriate `add_column` and `remove_column` statements will be created.
```bash
-$ rails generate migration AddPartNumberToProducts part_number:string
+$ bin/rails generate migration AddPartNumberToProducts part_number:string
```
will generate
@@ -153,7 +154,7 @@ end
If you'd like to add an index on the new column, you can do that as well:
```bash
-$ rails generate migration AddPartNumberToProducts part_number:string:index
+$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
```
will generate
@@ -171,7 +172,7 @@ end
Similarly, you can generate a migration to remove a column from the command line:
```bash
-$ rails generate migration RemovePartNumberFromProducts part_number:string
+$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
```
generates
@@ -187,7 +188,7 @@ end
You are not limited to one magically generated column. For example:
```bash
-$ rails generate migration AddDetailsToProducts part_number:string price:decimal
+$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
```
generates
@@ -206,7 +207,7 @@ followed by a list of column names and types then a migration creating the table
XXX with the columns listed will be generated. For example:
```bash
-$ rails generate migration CreateProducts name:string part_number:string
+$ bin/rails generate migration CreateProducts name:string part_number:string
```
generates
@@ -230,7 +231,7 @@ Also, the generator accepts column type as `references`(also available as
`belongs_to`). For instance:
```bash
-$ rails generate migration AddUserRefToProducts user:references
+$ bin/rails generate migration AddUserRefToProducts user:references
```
generates
@@ -248,7 +249,7 @@ This migration will create a `user_id` column and appropriate index.
There is also a generator which will produce join tables if `JoinTable` is part of the name:
```bash
-rails g migration CreateJoinTableCustomerProduct customer product
+$ bin/rails g migration CreateJoinTableCustomerProduct customer product
```
will produce the following migration:
@@ -272,7 +273,7 @@ relevant table. If you tell Rails what columns you want, then statements for
adding these columns will also be created. For example, running:
```bash
-$ rails generate model Product name:string description:text
+$ bin/rails generate model Product name:string description:text
```
will create a migration that looks like this
@@ -292,21 +293,15 @@ end
You can append as many column name/type pairs as you want.
-### Supported Type Modifiers
+### Passing Modifiers
-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, 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.
+Some commonly used [type modifiers](#column-modifiers) can be passed directly on
+the command line. They are enclosed by curly braces and follow the field type:
For instance, running:
```bash
-$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
+$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
```
will produce a migration that looks like this
@@ -320,6 +315,8 @@ class AddDetailsToProducts < ActiveRecord::Migration
end
```
+TIP: Have a look at the generators help output for further details.
+
Writing a Migration
-------------------
@@ -414,6 +411,85 @@ end
removes the `description` and `name` columns, creates a `part_number` string
column and adds an index on it. Finally it renames the `upccode` column.
+### Changing Columns
+
+Like the `remove_column` and `add_column` Rails provides the `change_column`
+migration method.
+
+```ruby
+change_column :products, :part_number, :text
+```
+
+This changes the column `part_number` on products table to be a `:text` field.
+
+Besides `change_column`, the `change_column_null` and `change_column_default`
+methods are used specifically to change the null and default values of a
+column.
+
+```ruby
+change_column_null :products, :name, false
+change_column_default :products, :approved, false
+```
+
+This sets `:name` field on products to a `NOT NULL` column and the default
+value of the `:approved` field to false.
+
+TIP: Unlike `change_column` (and `change_column_default`), `change_column_null`
+is reversible.
+
+### Column Modifiers
+
+Column modifiers can be applied when creating or changing a column:
+
+* `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.
+* `default` Allows to set a default value on the column. Note that if you
+are using a dynamic value (such as a date), the default will only be calculated
+the first time (i.e. on the date the migration is applied).
+* `index` Adds an index for the column.
+
+Some adapters may support additional options; see the adapter specific API docs
+for further information.
+
+### Foreign Keys
+
+While it's not required you might want to add foreign key constraints to
+[guarantee referential integrity](#active-record-and-referential-integrity).
+
+```ruby
+add_foreign_key :articles, :authors
+```
+
+This adds a new foreign key to the `author_id` column of the `articles`
+table. The key references the `id` column of the `articles` table. If the
+column names can not be derived from the table names, you can use the
+`:column` and `:primary_key` options.
+
+Rails will generate a name for every foreign key starting with
+`fk_rails_` followed by 10 random characters.
+There is a `:name` option to specify a different name if needed.
+
+NOTE: Active Record only supports single column foreign keys. `execute` and
+`structure.sql` are required to use composite foreign keys.
+
+Removing a foreign key is easy as well:
+
+```ruby
+# let Active Record figure out the column name
+remove_foreign_key :accounts, :branches
+
+# remove foreign key for a specific column
+remove_foreign_key :accounts, column: :owner_id
+
+# remove foreign key by name
+remove_foreign_key :accounts, name: :special_fk_name
+```
+
### When Helpers aren't Enough
If the helpers provided by Active Record aren't enough you can use the `execute`
@@ -444,6 +520,7 @@ definitions:
* `add_index`
* `add_reference`
* `add_timestamps`
+* `add_foreign_key`
* `create_table`
* `create_join_table`
* `drop_table` (must supply a block)
@@ -469,24 +546,23 @@ migration what else to do when reverting it. For example:
```ruby
class ExampleMigration < ActiveRecord::Migration
def change
- create_table :products do |t|
- t.references :category
+ create_table :distributors do |t|
+ t.string :zipcode
end
reversible do |dir|
dir.up do
- #add a foreign key
+ # add a CHECK constraint
execute <<-SQL
- ALTER TABLE products
- ADD CONSTRAINT fk_products_categories
- FOREIGN KEY (category_id)
- REFERENCES categories(id)
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5) NO INHERIT;
SQL
end
dir.down do
execute <<-SQL
- ALTER TABLE products
- DROP FOREIGN KEY fk_products_categories
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
SQL
end
end
@@ -494,12 +570,13 @@ 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
right order too. If the previous example migration is reverted,
the `down` block will be run after the `home_page_url` column is removed and
-right before the table `products` is dropped.
+right before the table `distributors` is dropped.
Sometimes your migration will do something which is just plain irreversible; for
example, it might destroy some data. In such cases, you can raise
@@ -522,16 +599,15 @@ made in the `up` method. The example in the `reversible` section is equivalent t
```ruby
class ExampleMigration < ActiveRecord::Migration
def up
- create_table :products do |t|
- t.references :category
+ create_table :distributors do |t|
+ t.string :zipcode
end
- # add a foreign key
+ # add a CHECK constraint
execute <<-SQL
- ALTER TABLE products
- ADD CONSTRAINT fk_products_categories
- FOREIGN KEY (category_id)
- REFERENCES categories(id)
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5);
SQL
add_column :users, :home_page_url, :string
@@ -543,11 +619,11 @@ class ExampleMigration < ActiveRecord::Migration
remove_column :users, :home_page_url
execute <<-SQL
- ALTER TABLE products
- DROP FOREIGN KEY fk_products_categories
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
SQL
- drop_table :products
+ drop_table :distributors
end
end
```
@@ -578,43 +654,27 @@ end
The `revert` method also accepts a block of instructions to reverse.
This could be useful to revert selected parts of previous migrations.
For example, let's imagine that `ExampleMigration` is committed and it
-is later decided it would be best to serialize the product list instead.
-One could write:
+is later decided it would be best to use Active Record validations,
+in place of the `CHECK` constraint, to verify the zipcode.
```ruby
-class SerializeProductListMigration < ActiveRecord::Migration
+class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration
def change
- add_column :categories, :product_list
-
- reversible do |dir|
- dir.up do
- # transfer data from Products to Category#product_list
- end
- dir.down do
- # create Products from Category#product_list
- end
- end
-
revert do
# copy-pasted code from ExampleMigration
- create_table :products do |t|
- t.references :category
- end
-
reversible do |dir|
dir.up do
- #add a foreign key
+ # add a CHECK constraint
execute <<-SQL
- ALTER TABLE products
- ADD CONSTRAINT fk_products_categories
- FOREIGN KEY (category_id)
- REFERENCES categories(id)
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5);
SQL
end
dir.down do
execute <<-SQL
- ALTER TABLE products
- DROP FOREIGN KEY fk_products_categories
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
SQL
end
end
@@ -651,7 +711,7 @@ is the numerical prefix on the migration's filename. For example, to migrate
to version 20080906120000 run:
```bash
-$ rake db:migrate VERSION=20080906120000
+$ bin/rake db:migrate VERSION=20080906120000
```
If version 20080906120000 is greater than the current version (i.e., it is
@@ -668,7 +728,7 @@ mistake in it and wish to correct it. Rather than tracking down the version
number associated with the previous migration you can run:
```bash
-$ rake db:rollback
+$ bin/rake db:rollback
```
This will rollback the latest migration, either by reverting the `change`
@@ -676,7 +736,7 @@ method or by running the `down` method. If you need to undo
several migrations you can provide a `STEP` parameter:
```bash
-$ rake db:rollback STEP=3
+$ bin/rake db:rollback STEP=3
```
will revert the last 3 migrations.
@@ -686,7 +746,7 @@ 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:
```bash
-$ rake db:migrate:redo STEP=3
+$ bin/rake db:migrate:redo STEP=3
```
Neither of these Rake tasks do anything you could not do with `db:migrate`. They
@@ -716,7 +776,7 @@ the corresponding migration will have its `change`, `up` or `down` method
invoked, for example:
```bash
-$ rake db:migrate:up VERSION=20080906120000
+$ bin/rake db:migrate:up VERSION=20080906120000
```
will run the 20080906120000 migration by running the `change` method (or the
@@ -732,7 +792,7 @@ To run migrations against another environment you can specify it using the
migrations against the `test` environment you could run:
```bash
-$ rake db:migrate RAILS_ENV=test
+$ bin/rake db:migrate RAILS_ENV=test
```
### Changing the Output of Running Migrations
@@ -818,159 +878,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:
-
-```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
- end
-end
-```
-
-She also adds a validation to the `Product` model for the new column:
-
-```ruby
-# app/models/product.rb
-
-class Product < ActiveRecord::Base
- validates :flag, inclusion: { in: [true, false] }
-end
-```
-
-Alice adds a second migration which adds another column to the `products`
-table and initializes it:
-
-```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
-```
-
-She also adds a validation to the `Product` model for the new column:
-
-```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 Active Record 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
----------------------
@@ -1032,10 +939,10 @@ that Active Record supports. This could be very useful if you were to
distribute an application that is able to run against multiple databases.
There is however a trade-off: `db/schema.rb` cannot express database specific
-items such as foreign key constraints, triggers, or stored procedures. While in
-a migration you can execute custom SQL statements, the schema dumper cannot
-reconstitute those statements from the database. If you are using features like
-this, then you should set the schema format to `:sql`.
+items such as triggers, or stored procedures. While in a migration you can
+execute custom SQL statements, the schema dumper cannot reconstitute those
+statements from the database. If you are using features like 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`
@@ -1053,11 +960,16 @@ schema into a RDBMS other than the one used to create it.
Because schema dumps are the authoritative source for your database schema, it
is strongly recommended that you check them into source control.
+`db/schema.rb` contains the current version number of the database. This
+ensures conflicts are going to happen in the case of a merge where both
+branches touched the schema. When that happens, solve conflicts manually,
+keeping the highest version number of the two.
+
Active Record and Referential Integrity
---------------------------------------
The Active Record way claims that intelligence belongs in your models, not in
-the database. As such, features such as triggers or foreign key constraints,
+the database. As such, features such as triggers or constraints,
which push some of that intelligence back into the database, are not heavily
used.
@@ -1066,14 +978,10 @@ which models can enforce data integrity. The `:dependent` option on
associations allows models to automatically destroy child objects when the
parent is destroyed. Like anything which operates at the application level,
these cannot guarantee referential integrity and so some people augment them
-with foreign key constraints in the database.
-
-Although Active Record does not provide any tools for working directly with
-such features, the `execute` method can be used to execute arbitrary SQL. You
-can also use a gem like
-[foreigner](https://github.com/matthuhiggins/foreigner) which adds foreign key
-support to Active Record (including support for dumping foreign keys in
-`db/schema.rb`).
+with [foreign key constraints](#foreign-keys) in the database.
+
+Although Active Record does not provide all the tools for working directly with
+such features, the `execute` method can be used to execute arbitrary SQL.
Migrations and Seed Data
------------------------
diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md
new file mode 100644
index 0000000000..a5649e3903
--- /dev/null
+++ b/guides/source/active_record_postgresql.md
@@ -0,0 +1,437 @@
+Active Record and PostgreSQL
+============================
+
+This guide covers PostgreSQL specific usage of Active Record.
+
+After reading this guide, you will know:
+
+* How to use PostgreSQL's datatypes.
+* How to use UUID primary keys.
+* How to implement full text search with PostgreSQL.
+* How to back your Active Record models with database views.
+
+--------------------------------------------------------------------------------
+
+In order to use the PostgreSQL adapter you need to have at least version 8.2
+installed. Older versions are not supported.
+
+To get started with PostgreSQL have a look at the
+[configuring Rails guide](configuring.html#configuring-a-postgresql-database).
+It describes how to properly setup Active Record for PostgreSQL.
+
+Datatypes
+---------
+
+PostgreSQL offers a number of specific datatypes. Following is a list of types,
+that are supported by the PostgreSQL adapter.
+
+### Bytea
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html)
+
+```ruby
+# db/migrate/20140207133952_create_documents.rb
+create_table :documents do |t|
+ t.binary 'payload'
+end
+
+# app/models/document.rb
+class Document < ActiveRecord::Base
+end
+
+# Usage
+data = File.read(Rails.root + "tmp/output.pdf")
+Document.create payload: data
+```
+
+### Array
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html)
+
+```ruby
+# db/migrate/20140207133952_create_books.rb
+create_table :books do |t|
+ t.string 'title'
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
+end
+add_index :books, :tags, using: 'gin'
+add_index :books, :ratings, using: 'gin'
+
+# app/models/book.rb
+class Book < ActiveRecord::Base
+end
+
+# Usage
+Book.create title: "Brave New World",
+ tags: ["fantasy", "fiction"],
+ ratings: [4, 5]
+
+## Books for a single tag
+Book.where("'fantasy' = ANY (tags)")
+
+## Books for multiple tags
+Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
+
+## Books with 3 or more ratings
+Book.where("array_length(ratings, 1) >= 3")
+```
+
+### Hstore
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html)
+
+```ruby
+# db/migrate/20131009135255_create_profiles.rb
+ActiveRecord::Schema.define do
+ create_table :profiles do |t|
+ t.hstore 'settings'
+ end
+end
+
+# app/models/profile.rb
+class Profile < ActiveRecord::Base
+end
+
+# Usage
+Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
+
+profile = Profile.first
+profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
+
+profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
+profile.save!
+
+## you need to call _will_change! if you are editing the store in place
+profile.settings["color"] = "green"
+profile.settings_will_change!
+profile.save!
+```
+
+### JSON
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html)
+
+```ruby
+# db/migrate/20131220144913_create_events.rb
+create_table :events do |t|
+ t.json 'payload'
+end
+
+# app/models/event.rb
+class Event < ActiveRecord::Base
+end
+
+# Usage
+Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
+
+event = Event.first
+event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
+
+## Query based on JSON document
+Event.where("payload->'kind' = ?", "user_renamed")
+```
+
+### Range Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html)
+
+This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects.
+
+```ruby
+# db/migrate/20130923065404_create_events.rb
+create_table :events do |t|
+ t.daterange 'duration'
+end
+
+# app/models/event.rb
+class Event < ActiveRecord::Base
+end
+
+# Usage
+Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12))
+
+event = Event.first
+event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014
+
+## All Events on a given date
+Event.where("duration @> ?::date", Date.new(2014, 2, 12))
+
+## Working with range bounds
+event = Event.
+ select("lower(duration) AS starts_at").
+ select("upper(duration) AS ends_at").first
+
+event.starts_at # => Tue, 11 Feb 2014
+event.ends_at # => Thu, 13 Feb 2014
+```
+
+### Composite Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html)
+
+Currently there is no special support for composite types. They are mapped to
+normal text columns:
+
+```sql
+CREATE TYPE full_address AS
+(
+ city VARCHAR(90),
+ street VARCHAR(90)
+);
+```
+
+```ruby
+# db/migrate/20140207133952_create_contacts.rb
+execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+ city VARCHAR(90),
+ street VARCHAR(90)
+ );
+SQL
+create_table :contacts do |t|
+ t.column :address, :full_address
+end
+
+# app/models/contact.rb
+class Contact < ActiveRecord::Base
+end
+
+# Usage
+Contact.create address: "(Paris,Champs-Élysées)"
+contact = Contact.first
+contact.address # => "(Paris,Champs-Élysées)"
+contact.address = "(Paris,Rue Basse)"
+contact.save!
+```
+
+### Enumerated Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html)
+
+Currently there is no special support for enumerated types. They are mapped as
+normal text columns:
+
+```ruby
+# db/migrate/20131220144913_create_events.rb
+execute <<-SQL
+ CREATE TYPE article_status AS ENUM ('draft', 'published');
+SQL
+create_table :articles do |t|
+ t.column :status, :article_status
+end
+
+# app/models/article.rb
+class Article < ActiveRecord::Base
+end
+
+# Usage
+Article.create status: "draft"
+article = Article.first
+article.status # => "draft"
+
+article.status = "published"
+article.save!
+```
+
+### UUID
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html)
+* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html)
+
+
+```ruby
+# db/migrate/20131220144913_create_revisions.rb
+create_table :revisions do |t|
+ t.column :identifier, :uuid
+end
+
+# app/models/revision.rb
+class Revision < ActiveRecord::Base
+end
+
+# Usage
+Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"
+
+revision = Revision.first
+revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
+```
+
+### Bit String Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html)
+
+```ruby
+# db/migrate/20131220144913_create_users.rb
+create_table :users, force: true do |t|
+ t.column :settings, "bit(8)"
+end
+
+# app/models/device.rb
+class User < ActiveRecord::Base
+end
+
+# Usage
+User.create settings: "01010011"
+user = User.first
+user.settings # => "(Paris,Champs-Élysées)"
+user.settings = "0xAF"
+user.settings # => 10101111
+user.save!
+```
+
+### Network Address Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html)
+
+The types `inet` and `cidr` are mapped to Ruby
+[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html)
+objects. The `macaddr` type is mapped to normal text.
+
+```ruby
+# db/migrate/20140508144913_create_devices.rb
+create_table(:devices, force: true) do |t|
+ t.inet 'ip'
+ t.cidr 'network'
+ t.macaddr 'address'
+end
+
+# app/models/device.rb
+class Device < ActiveRecord::Base
+end
+
+# Usage
+macbook = Device.create(ip: "192.168.1.12",
+ network: "192.168.2.0/24",
+ address: "32:01:16:6d:05:ef")
+
+macbook.ip
+# => #<IPAddr: IPv4:192.168.1.12/255.255.255.255>
+
+macbook.network
+# => #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
+
+macbook.address
+# => "32:01:16:6d:05:ef"
+```
+
+### Geometric Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html)
+
+All geometric types, with the exception of `points` are mapped to normal text.
+A point is casted to an array containing `x` and `y` coordinates.
+
+
+UUID Primary Keys
+-----------------
+
+NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs.
+
+```ruby
+# db/migrate/20131220144913_create_devices.rb
+enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp')
+create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t|
+ t.string :kind
+end
+
+# app/models/device.rb
+class Device < ActiveRecord::Base
+end
+
+# Usage
+device = Device.create
+device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e"
+```
+
+Full Text Search
+----------------
+
+```ruby
+# db/migrate/20131220144913_create_documents.rb
+create_table :documents do |t|
+ t.string 'title'
+ t.string 'body'
+end
+
+execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));"
+
+# app/models/document.rb
+class Document < ActiveRecord::Base
+end
+
+# Usage
+Document.create(title: "Cats and Dogs", body: "are nice!")
+
+## all documents matching 'cat & dog'
+Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
+ "cat & dog")
+```
+
+Database Views
+--------------
+
+* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html)
+
+Imagine you need to work with a legacy database containing the following table:
+
+```
+rails_pg_guide=# \d "TBL_ART"
+ Table "public.TBL_ART"
+ Column | Type | Modifiers
+------------+-----------------------------+------------------------------------------------------------
+ INT_ID | integer | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
+ STR_TITLE | character varying |
+ STR_STAT | character varying | default 'draft'::character varying
+ DT_PUBL_AT | timestamp without time zone |
+ BL_ARCH | boolean | default false
+Indexes:
+ "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")
+```
+
+This table does not follow the Rails conventions at all.
+Because simple PostgreSQL views are updateable by default,
+we can wrap it as follows:
+
+```ruby
+# db/migrate/20131220144913_create_articles_view.rb
+execute <<-SQL
+CREATE VIEW articles AS
+ SELECT "INT_ID" AS id,
+ "STR_TITLE" AS title,
+ "STR_STAT" AS status,
+ "DT_PUBL_AT" AS published_at,
+ "BL_ARCH" AS archived
+ FROM "TBL_ART"
+ WHERE "BL_ARCH" = 'f'
+ SQL
+
+# app/models/article.rb
+class Article < ActiveRecord::Base
+ self.primary_key = "id"
+ def archive!
+ update_attribute :archived, true
+ end
+end
+
+# Usage
+first = Article.create! title: "Winter is coming",
+ status: "published",
+ published_at: 1.year.ago
+second = Article.create! title: "Brace yourself",
+ status: "draft",
+ published_at: 1.month.ago
+
+Article.count # => 1
+first.archive!
+Article.count # => 2
+```
+
+NOTE: This application only cares about non-archived `Articles`. A view also
+allows for conditions so we can exclude the archived `Articles` directly.
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index d164b08d93..35467fe95b 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -93,9 +93,9 @@ The primary operation of `Model.find(options)` can be summarized as:
Active Record provides several different ways of retrieving a single object.
-#### Using a Primary Key
+#### `find`
-Using `Model.find(primary_key)`, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example:
+Using the `find` method, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example:
```ruby
# Find the client with primary key (id) 10.
@@ -109,119 +109,103 @@ The SQL equivalent of the above is:
SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
```
-`Model.find(primary_key)` will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found.
+The `find` method will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found.
-#### `take`
-
-`Model.take` retrieves a record without any implicit ordering. For example:
+You can also use this method to query for multiple objects. Call the `find` method and pass in an array of primary keys. The return will be an array containing all of the matching records for the supplied _primary keys_. For example:
```ruby
-client = Client.take
-# => #<Client id: 1, first_name: "Lifo">
+# Find the clients with primary keys 1 and 10.
+client = Client.find([1, 10]) # Or even Client.find(1, 10)
+# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients LIMIT 1
+SELECT * FROM clients WHERE (clients.id IN (1,10))
```
-`Model.take` returns `nil` if no record is found and no exception will be raised.
-
-TIP: The retrieved record may vary depending on the database engine.
+WARNING: The `find` method will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys.
-#### `first`
+#### `take`
-`Model.first` finds the first record ordered by the primary key. For example:
+The `take` method retrieves a record without any implicit ordering. For example:
```ruby
-client = Client.first
+client = Client.take
# => #<Client id: 1, first_name: "Lifo">
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+SELECT * FROM clients LIMIT 1
```
-`Model.first` returns `nil` if no matching record is found and no exception will be raised.
+The `take` method returns `nil` if no record is found and no exception will be raised.
-#### `last`
-
-`Model.last` finds the last record ordered by the primary key. For example:
+You can pass in a numerical argument to the `take` method to return up to that number of results. For example
```ruby
-client = Client.last
-# => #<Client id: 221, first_name: "Russel">
+client = Client.take(2)
+# => [
+ #<Client id: 1, first_name: "Lifo">,
+ #<Client id: 220, first_name: "Sara">
+]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
-```
-
-`Model.last` returns `nil` if no matching record is found and no exception will be raised.
-
-#### `find_by`
-
-`Model.find_by` finds the first record matching some conditions. For example:
-
-```ruby
-Client.find_by first_name: 'Lifo'
-# => #<Client id: 1, first_name: "Lifo">
-
-Client.find_by first_name: 'Jon'
-# => nil
+SELECT * FROM clients LIMIT 2
```
-It is equivalent to writing:
+The `take!` method behaves exactly like `take`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
-```ruby
-Client.where(first_name: 'Lifo').take
-```
+TIP: The retrieved record may vary depending on the database engine.
-#### `take!`
+#### `first`
-`Model.take!` retrieves a record without any implicit ordering. For example:
+The `first` method finds the first record ordered by the primary key. For example:
```ruby
-client = Client.take!
+client = Client.first
# => #<Client id: 1, first_name: "Lifo">
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients LIMIT 1
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
```
-`Model.take!` raises `ActiveRecord::RecordNotFound` if no matching record is found.
+The `first` method returns `nil` if no matching record is found and no exception will be raised.
-#### `first!`
-
-`Model.first!` finds the first record ordered by the primary key. For example:
+You can pass in a numerical argument to the `first` method to return up to that number of results. For example
```ruby
-client = Client.first!
-# => #<Client id: 1, first_name: "Lifo">
+client = Client.first(3)
+# => [
+ #<Client id: 1, first_name: "Lifo">,
+ #<Client id: 2, first_name: "Fifo">,
+ #<Client id: 3, first_name: "Filo">
+]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
```
-`Model.first!` raises `ActiveRecord::RecordNotFound` if no matching record is found.
+The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
-#### `last!`
+#### `last`
-`Model.last!` finds the last record ordered by the primary key. For example:
+The `last` method finds the last record ordered by the primary key. For example:
```ruby
-client = Client.last!
+client = Client.last
# => #<Client id: 221, first_name: "Russel">
```
@@ -231,92 +215,56 @@ The SQL equivalent of the above is:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
```
-`Model.last!` raises `ActiveRecord::RecordNotFound` if no matching record is found.
-
-#### `find_by!`
-
-`Model.find_by!` finds the first record matching some conditions. It raises `ActiveRecord::RecordNotFound` if no matching record is found. For example:
-
-```ruby
-Client.find_by! first_name: 'Lifo'
-# => #<Client id: 1, first_name: "Lifo">
-
-Client.find_by! first_name: 'Jon'
-# => ActiveRecord::RecordNotFound
-```
-
-It is equivalent to writing:
+The `last` method returns `nil` if no matching record is found and no exception will be raised.
-```ruby
-Client.where(first_name: 'Lifo').take!
-```
-
-### Retrieving Multiple Objects
-
-#### Using Multiple Primary Keys
-
-`Model.find(array_of_primary_key)` accepts an array of _primary keys_, returning an array containing all of the matching records for the supplied _primary keys_. For example:
+You can pass in a numerical argument to the `last` method to return up to that number of results. For example
```ruby
-# Find the clients with primary keys 1 and 10.
-client = Client.find([1, 10]) # Or even Client.find(1, 10)
-# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
+client = Client.last(3)
+# => [
+ #<Client id: 219, first_name: "James">,
+ #<Client id: 220, first_name: "Sara">,
+ #<Client id: 221, first_name: "Russel">
+]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients WHERE (clients.id IN (1,10))
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
```
-WARNING: `Model.find(array_of_primary_key)` will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys.
+The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
-#### take
+#### `find_by`
-`Model.take(limit)` retrieves the first number of records specified by `limit` without any explicit ordering:
+The `find_by` method finds the first record matching some conditions. For example:
```ruby
-Client.take(2)
-# => [#<Client id: 1, first_name: "Lifo">,
- #<Client id: 2, first_name: "Raf">]
-```
-
-The SQL equivalent of the above is:
+Client.find_by first_name: 'Lifo'
+# => #<Client id: 1, first_name: "Lifo">
-```sql
-SELECT * FROM clients LIMIT 2
+Client.find_by first_name: 'Jon'
+# => nil
```
-#### first
-
-`Model.first(limit)` finds the first number of records specified by `limit` ordered by primary key:
+It is equivalent to writing:
```ruby
-Client.first(2)
-# => [#<Client id: 1, first_name: "Lifo">,
- #<Client id: 2, first_name: "Raf">]
-```
-
-The SQL equivalent of the above is:
-
-```sql
-SELECT * FROM clients ORDER BY id ASC LIMIT 2
+Client.where(first_name: 'Lifo').take
```
-#### last
-
-`Model.last(limit)` finds the number of records specified by `limit` ordered by primary key in descending order:
+The `find_by!` method behaves exactly like `find_by`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. For example:
```ruby
-Client.last(2)
-# => [#<Client id: 10, first_name: "Ryan">,
- #<Client id: 9, first_name: "John">]
+Client.find_by! first_name: 'does not exist'
+# => ActiveRecord::RecordNotFound
```
-The SQL equivalent of the above is:
+This is equivalent to writing:
-```sql
-SELECT * FROM clients ORDER BY id DESC LIMIT 2
+```ruby
+Client.where(first_name: 'does not exist').take!
```
### Retrieving Multiple Objects in Batches
@@ -328,7 +276,7 @@ This may appear straightforward:
```ruby
# This is very inefficient when the users table has thousands of rows.
User.all.each do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver
end
```
@@ -344,7 +292,15 @@ The `find_each` method retrieves a batch of records and then yields _each_ recor
```ruby
User.find_each do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver
+end
+```
+
+To add conditions to a `find_each` operation you can chain other Active Record methods such as `where`:
+
+```ruby
+User.where(weekly_subscriber: true).find_each do |user|
+ NewsMailer.weekly(user).deliver
end
```
@@ -360,7 +316,7 @@ The `:batch_size` option allows you to specify the number of records to be retri
```ruby
User.find_each(batch_size: 5000) do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver
end
```
@@ -372,7 +328,7 @@ For example, to send newsletters only to users with the primary key starting fro
```ruby
User.find_each(start: 2000, batch_size: 5000) do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver
end
```
@@ -472,8 +428,8 @@ Client.where('locked' => true)
In the case of a belongs_to relationship, an association key can be used to specify the model if an Active Record object is used as the value. This method works with polymorphic relationships as well.
```ruby
-Post.where(author: author)
-Author.joins(:posts).where(posts: { author: author })
+Article.where(author: author)
+Author.joins(:articles).where(articles: { author: author })
```
NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`.
@@ -511,7 +467,7 @@ SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
`NOT` SQL queries can be built by `where.not`.
```ruby
-Post.where.not(author: author)
+Article.where.not(author: author)
```
In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions.
@@ -659,6 +615,23 @@ FROM orders
GROUP BY date(created_at)
```
+### Total of grouped items
+
+To get the total of grouped items on a single query call `count` after the `group`.
+
+```ruby
+Order.group(:status).count
+# => { 'awaiting_approval' => 7, 'paid' => 12 }
+```
+
+The SQL that would be executed would be something like this:
+
+```sql
+SELECT COUNT (*) AS count_all, status AS status
+FROM "orders"
+GROUP BY status
+```
+
Having
------
@@ -690,32 +663,32 @@ Overriding Conditions
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)
+Article.where('id > 10').limit(20).order('id asc').unscope(:order)
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE id > 10 LIMIT 20
+SELECT * FROM articles WHERE id > 10 LIMIT 20
# Original query without `unscope`
-SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20
+SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
```
-You can additionally unscope specific where clauses. For example:
+You can also unscope specific `where` clauses. For example:
```ruby
-Post.where(id: 10, trashed: false).unscope(where: :id)
-# => SELECT "posts".* FROM "posts" WHERE trashed = 0
+Article.where(id: 10, trashed: false).unscope(where: :id)
+# SELECT "articles".* FROM "articles" WHERE trashed = 0
```
A relation which has used `unscope` will affect any relation it is
merged in to:
```ruby
-Post.order('id asc').merge(Post.unscope(:order))
-# => SELECT "posts".* FROM "posts"
+Article.order('id asc').merge(Article.unscope(:order))
+# SELECT "articles".* FROM "articles"
```
### `only`
@@ -723,16 +696,16 @@ Post.order('id asc').merge(Post.unscope(:order))
You can also override conditions using the `only` method. For example:
```ruby
-Post.where('id > 10').limit(20).order('id desc').only(:order, :where)
+Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
+SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
# Original query without `only`
-SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20
+SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
```
@@ -741,25 +714,25 @@ SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20
The `reorder` method overrides the default scope order. For example:
```ruby
-class Post < ActiveRecord::Base
- ..
- ..
+class Article < ActiveRecord::Base
has_many :comments, -> { order('posted_at DESC') }
end
-Post.find(10).comments.reorder('name')
+Article.find(10).comments.reorder('name')
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE id = 10 ORDER BY name
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY name
```
In case the `reorder` clause is not used, the SQL executed would be:
```sql
-SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
```
### `reverse_order`
@@ -795,25 +768,25 @@ This method accepts **no** arguments.
The `rewhere` method overrides an existing, named where condition. For example:
```ruby
-Post.where(trashed: true).rewhere(trashed: false)
+Article.where(trashed: true).rewhere(trashed: false)
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE `trashed` = 0
+SELECT * FROM articles WHERE `trashed` = 0
```
In case the `rewhere` clause is not used,
```ruby
-Post.where(trashed: true).where(trashed: false)
+Article.where(trashed: true).where(trashed: false)
```
the SQL executed would be:
```sql
-SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0
+SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
```
Null Relation
@@ -822,21 +795,21 @@ Null Relation
The `none` method returns a chainable relation with no records. Any subsequent conditions chained to the returned relation will continue generating empty relations. This is useful in scenarios where you need a chainable response to a method or a scope that could return zero results.
```ruby
-Post.none # returns an empty Relation and fires no queries.
+Article.none # returns an empty Relation and fires no queries.
```
```ruby
-# The visible_posts method below is expected to return a Relation.
-@posts = current_user.visible_posts.where(name: params[:name])
+# The visible_articles method below is expected to return a Relation.
+@articles = current_user.visible_articles.where(name: params[:name])
-def visible_posts
+def visible_articles
case role
when 'Country Manager'
- Post.where(country: country)
+ Article.where(country: country)
when 'Reviewer'
- Post.published
+ Article.published
when 'Bad User'
- Post.none # => returning [] or nil breaks the caller code in this case
+ Article.none # => returning [] or nil breaks the caller code in this case
end
end
```
@@ -961,23 +934,23 @@ 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`, `Comment`, `Guest` and `Tag` models:
+For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models:
```ruby
class Category < ActiveRecord::Base
- has_many :posts
+ has_many :articles
end
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
has_one :guest
end
@@ -986,7 +959,7 @@ class Guest < ActiveRecord::Base
end
class Tag < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
end
```
@@ -995,64 +968,64 @@ Now all of the following will produce the expected join queries using `INNER JOI
#### Joining a Single Association
```ruby
-Category.joins(:posts)
+Category.joins(:articles)
```
This produces:
```sql
SELECT categories.* FROM categories
- INNER JOIN posts ON posts.category_id = categories.id
+ INNER JOIN articles ON articles.category_id = categories.id
```
-Or, in English: "return a Category object for all categories with posts". Note that you will see duplicate categories if more than one post has the same category. If you want unique categories, you can use `Category.joins(:posts).uniq`.
+Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`.
#### Joining Multiple Associations
```ruby
-Post.joins(:category, :comments)
+Article.joins(:category, :comments)
```
This produces:
```sql
-SELECT posts.* FROM posts
- INNER JOIN categories ON posts.category_id = categories.id
- INNER JOIN comments ON comments.post_id = posts.id
+SELECT articles.* FROM articles
+ INNER JOIN categories ON articles.category_id = categories.id
+ INNER JOIN comments ON comments.article_id = articles.id
```
-Or, in English: "return all posts that have a category and at least one comment". Note again that posts with multiple comments will show up multiple times.
+Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times.
#### Joining Nested Associations (Single Level)
```ruby
-Post.joins(comments: :guest)
+Article.joins(comments: :guest)
```
This produces:
```sql
-SELECT posts.* FROM posts
- INNER JOIN comments ON comments.post_id = posts.id
+SELECT articles.* FROM articles
+ INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
```
-Or, in English: "return all posts that have a comment made by a guest."
+Or, in English: "return all articles that have a comment made by a guest."
#### Joining Nested Associations (Multiple Level)
```ruby
-Category.joins(posts: [{ comments: :guest }, :tags])
+Category.joins(articles: [{ comments: :guest }, :tags])
```
This produces:
```sql
SELECT categories.* FROM categories
- INNER JOIN posts ON posts.category_id = categories.id
- INNER JOIN comments ON comments.post_id = posts.id
+ INNER JOIN articles ON articles.category_id = categories.id
+ INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
- INNER JOIN tags ON tags.post_id = posts.id
+ INNER JOIN tags ON tags.article_id = articles.id
```
### Specifying Conditions on the Joined Tables
@@ -1121,18 +1094,18 @@ Active Record lets you eager load any number of associations with a single `Mode
#### Array of Multiple Associations
```ruby
-Post.includes(:category, :comments)
+Article.includes(:category, :comments)
```
-This loads all the posts and the associated category and comments for each post.
+This loads all the articles and the associated category and comments for each article.
#### Nested Associations Hash
```ruby
-Category.includes(posts: [{ comments: :guest }, :tags]).find(1)
+Category.includes(articles: [{ 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.
+This will find the category with id 1 and eager load all of the associated articles, the associated articles' tags and comments, and every comment's guest association.
### Specifying Conditions on Eager Loaded Associations
@@ -1141,18 +1114,31 @@ Even though Active Record lets you specify conditions on the eager loaded associ
However if you must do this, you may use `where` as you would normally.
```ruby
-Post.includes(:comments).where("comments.visible" => true)
+Article.includes(:comments).where(comments: { visible: true })
```
-This would generate a query which contains a `LEFT OUTER JOIN` whereas the `joins` method would generate one using the `INNER JOIN` function instead.
+This would generate a query which contains a `LEFT OUTER JOIN` whereas the
+`joins` method would generate one using the `INNER JOIN` function instead.
```ruby
- SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1)
+ SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)
```
If there was no `where` condition, this would generate the normal set of two queries.
-If, in the case of this `includes` query, there were no comments for any posts, all the posts would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned.
+NOTE: Using `where` like this will only work when you pass it a Hash. For
+SQL-fragments you need use `references` to force joined tables:
+
+```ruby
+Article.includes(:comments).where("comments.visible = true").references(:comments)
+```
+
+If, in the case of this `includes` query, there were no comments for any
+articles, all the articles would still be loaded. By using `joins` (an INNER
+JOIN), the join conditions **must** match, otherwise no records will be
+returned.
+
+
Scopes
------
@@ -1162,7 +1148,7 @@ Scoping allows you to specify commonly-used queries which can be referenced as m
To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
end
```
@@ -1170,7 +1156,7 @@ end
This is exactly the same as defining a class method, and which you use is a matter of personal preference:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
def self.published
where(published: true)
end
@@ -1180,7 +1166,7 @@ end
Scopes are also chainable within scopes:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
@@ -1189,14 +1175,14 @@ end
To call this `published` scope we can call it on either the class:
```ruby
-Post.published # => [published posts]
+Article.published # => [published articles]
```
-Or on an association consisting of `Post` objects:
+Or on an association consisting of `Article` objects:
```ruby
category = Category.first
-category.posts.published # => [published posts belonging to this category]
+category.articles.published # => [published articles belonging to this category]
```
### Passing in arguments
@@ -1204,7 +1190,7 @@ category.posts.published # => [published posts belonging to this category]
Your scope can take arguments:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
scope :created_before, ->(time) { where("created_at < ?", time) }
end
```
@@ -1212,13 +1198,13 @@ end
Call the scope as if it were a class method:
```ruby
-Post.created_before(Time.zone.now)
+Article.created_before(Time.zone.now)
```
However, this is just duplicating the functionality that would be provided to you by a class method.
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
def self.created_before(time)
where("created_at < ?", time)
end
@@ -1228,7 +1214,36 @@ end
Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects:
```ruby
-category.posts.created_before(time)
+category.articles.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
@@ -1242,26 +1257,26 @@ class User < ActiveRecord::Base
end
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
@@ -1272,48 +1287,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
@@ -1456,6 +1441,11 @@ If you'd like to use your own SQL to find records in a table you can use `find_b
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER BY clients.created_at desc")
+# => [
+ #<Client id: 1, first_name: "Lucas" >,
+ #<Client id: 2, first_name: "Jan" >,
+ # ...
+]
```
`find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
@@ -1465,7 +1455,11 @@ Client.find_by_sql("SELECT * FROM clients
`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record.
```ruby
-Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
+Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
+# => [
+ {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
+ {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
+]
```
### `pluck`
@@ -1592,20 +1586,20 @@ You can also use `any?` and `many?` to check for existence on a model or relatio
```ruby
# via a model
-Post.any?
-Post.many?
+Article.any?
+Article.many?
# via a named scope
-Post.recent.any?
-Post.recent.many?
+Article.recent.any?
+Article.recent.many?
# via a relation
-Post.where(published: true).any?
-Post.where(published: true).many?
+Article.where(published: true).any?
+Article.where(published: true).many?
# via an association
-Post.first.categories.any?
-Post.first.categories.many?
+Article.first.categories.any?
+Article.first.categories.many?
```
Calculations
@@ -1695,19 +1689,26 @@ Running EXPLAIN
You can run EXPLAIN on the queries triggered by relations. For example,
```ruby
-User.where(id: 1).joins(:posts).explain
+User.where(id: 1).joins(:articles).explain
```
may yield
```
-EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
-| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
-| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
-| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
++----+-------------+----------+-------+---------------+
+| id | select_type | table | type | possible_keys |
++----+-------------+----------+-------+---------------+
+| 1 | SIMPLE | users | const | PRIMARY |
+| 1 | SIMPLE | articles | ALL | NULL |
++----+-------------+----------+-------+---------------+
++---------+---------+-------+------+-------------+
+| key | key_len | ref | rows | Extra |
++---------+---------+-------+------+-------------+
+| PRIMARY | 4 | const | 1 | |
+| NULL | NULL | NULL | 1 | Using where |
++---------+---------+-------+------+-------------+
+
2 rows in set (0.00 sec)
```
@@ -1717,15 +1718,15 @@ Active Record performs a pretty printing that emulates the one of the database
shells. So, the same query running with the PostgreSQL adapter would yield instead
```
-EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1
+EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1
QUERY PLAN
------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
- Join Filter: (posts.user_id = users.id)
+ Join Filter: (articles.user_id = users.id)
-> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
Index Cond: (id = 1)
- -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
- Filter: (posts.user_id = 1)
+ -> Seq Scan on articles (cost=0.00..28.88 rows=8 width=4)
+ Filter: (articles.user_id = 1)
(6 rows)
```
@@ -1734,26 +1735,39 @@ may need the results of previous ones. Because of that, `explain` actually
executes the query, and then asks for the query plans. For example,
```ruby
-User.where(id: 1).includes(:posts).explain
+User.where(id: 1).includes(:articles).explain
```
yields
```
EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
-| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
-| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
++----+-------------+-------+-------+---------------+
+| id | select_type | table | type | possible_keys |
++----+-------------+-------+-------+---------------+
+| 1 | SIMPLE | users | const | PRIMARY |
++----+-------------+-------+-------+---------------+
++---------+---------+-------+------+-------+
+| key | key_len | ref | rows | Extra |
++---------+---------+-------+------+-------+
+| PRIMARY | 4 | const | 1 | |
++---------+---------+-------+------+-------+
+
1 row in set (0.00 sec)
-EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1)
-+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
-| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
-+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
-| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
-+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
+EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1)
++----+-------------+----------+------+---------------+
+| id | select_type | table | type | possible_keys |
++----+-------------+----------+------+---------------+
+| 1 | SIMPLE | articles | ALL | NULL |
++----+-------------+----------+------+---------------+
++------+---------+------+------+-------------+
+| key | key_len | ref | rows | Extra |
++------+---------+------+------+-------------+
+| NULL | NULL | NULL | 1 | Using where |
++------+---------+------+------+-------------+
+
+
1 row in set (0.00 sec)
```
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index a483a6dd24..582bb240dd 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -85,7 +85,7 @@ end
We can see how it works by looking at some `rails console` output:
```ruby
-$ rails console
+$ bin/rails console
>> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
>> p.new_record?
@@ -871,7 +871,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and
```ruby
class Computer < ActiveRecord::Base
validates :mouse, presence: true,
- if: ["market.retail?", :desktop?]
+ if: ["market.retail?", :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
```
@@ -910,8 +910,8 @@ end
The easiest way to add custom validators for validating individual attributes
is with the convenient `ActiveModel::EachValidator`. In this case, the custom
validator class must implement a `validate_each` method which takes three
-arguments: record, attribute and value which correspond to the instance, the
-attribute to be validated and the value of the attribute in the passed
+arguments: record, attribute, and value. These correspond to the instance, the
+attribute to be validated, and the value of the attribute in the passed
instance.
```ruby
@@ -1129,15 +1129,15 @@ generating a scaffold, Rails will put some ERB into the `_form.html.erb` that
it generates that displays the full list of errors on that model.
Assuming we have a model that's been saved in an instance variable named
-`@post`, it looks like this:
+`@article`, it looks like this:
```ruby
-<% if @post.errors.any? %>
+<% if @article.errors.any? %>
<div id="error_explanation">
- <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
+ <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
- <% @post.errors.full_messages.each do |msg| %>
+ <% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
@@ -1151,7 +1151,7 @@ the entry.
```
<div class="field_with_errors">
- <input id="post_title" name="post[title]" size="30" type="text" value="">
+ <input id="article_title" name="article[title]" size="30" type="text" value="">
</div>
```
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 2ad09f599b..5ed392d43d 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -157,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.
@@ -572,12 +572,12 @@ NOTE: Defined in `active_support/core_ext/module/aliasing.rb`.
#### `alias_attribute`
-Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (my mnemonic is they go in the same order as if you did an assignment):
+Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment):
```ruby
class User < ActiveRecord::Base
- # let me refer to the email column as "login",
- # possibly meaningful for authentication code
+ # You can refer to the email column as "login".
+ # This can be meaningful for authentication code.
alias_attribute :login, :email
end
```
@@ -761,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
@@ -792,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`.
@@ -964,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`
@@ -1119,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/module/attribute_accessors.rb`. `active_support/core_ext/class/attribute_accessors.rb` is deprecated and will be removed in Ruby on Rails 4.2.
+NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`.
### Subclasses & Descendants
@@ -1178,9 +1165,9 @@ Inserting data into HTML templates needs extra care. For example, you can't just
#### Safe Strings
-Active Support has the concept of <i>(html) safe</i> strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not.
+Active Support has the concept of _(html) safe_ strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not.
-Strings are considered to be <i>unsafe</i> by default:
+Strings are considered to be _unsafe_ by default:
```ruby
"".html_safe? # => false
@@ -1644,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:
@@ -1778,21 +1768,36 @@ NOTE: Defined in `active_support/core_ext/string/inflections.rb`.
#### `humanize`
-The method `humanize` gives you a sensible name for display out of an attribute name. To do so it replaces underscores with spaces, removes any "_id" suffix, and capitalizes the first word:
+The method `humanize` tweaks an attribute name for display to end users.
+
+Specifically performs these transformations:
+
+ * Applies human inflection rules to the argument.
+ * Deletes leading underscores, if any.
+ * Removes a "_id" suffix if present.
+ * Replaces underscores with spaces, if any.
+ * Downcases all words except acronyms.
+ * Capitalizes the first word.
+
+The capitalization of the first word can be turned off by setting the
++:capitalize+ option to false (default is true).
```ruby
-"name".humanize # => "Name"
-"author_id".humanize # => "Author"
-"comments_count".humanize # => "Comments count"
+"name".humanize # => "Name"
+"author_id".humanize # => "Author"
+"author_id".humanize(capitalize: false) # => "author"
+"comments_count".humanize # => "Comments count"
+"_id".humanize # => "Id"
```
-The capitalization of the first word can be turned off by setting the optional parameter `capitalize` to false:
+If "SSL" was defined to be an acronym:
```ruby
-"author_id".humanize(capitalize: false) # => "author"
+'ssl_error'.humanize # => "SSL error"
```
-The helper method `full_messages` uses `humanize` as a fallback to include attribute names:
+The helper method `full_messages` uses `humanize` as a fallback to include
+attribute names:
```ruby
def full_messages
@@ -2719,11 +2724,14 @@ The method `transform_keys` accepts a block and returns a hash that has applied
# => {"" => 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
+# 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:
@@ -2758,11 +2766,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:
@@ -2799,11 +2810,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
@@ -2911,7 +2925,7 @@ 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.
+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}
@@ -3658,9 +3672,9 @@ t.advance(seconds: 1)
#### `Time.current`
-Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines `Time.yesterday` and `Time.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`.
+Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`.
-When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` and not `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.today` uses by default. This means `Time.now` may equal `Time.yesterday`.
+When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` instead of `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.now` uses by default. This means `Time.now.to_date` may equal `Date.yesterday`.
#### `all_day`, `all_week`, `all_month`, `all_quarter` and `all_year`
@@ -3824,7 +3838,7 @@ The name may be given as a symbol or string. A symbol is tested against the bare
TIP: A symbol can represent a fully-qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically.
-For example, when an action of `PostsController` is called Rails tries optimistically to use `PostsHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `posts_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases:
+For example, when an action of `ArticlesController` is called Rails tries optimistically to use `ArticlesHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `articles_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases:
```ruby
def default_helper_module!
@@ -3838,7 +3852,7 @@ rescue NameError => e
end
```
-NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`.
+NOTE: Defined in `active_support/core_ext/name_error.rb`.
Extensions to `LoadError`
-------------------------
@@ -3847,7 +3861,7 @@ Active Support adds `is_missing?` to `LoadError`, and also assigns that class to
Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension).
-For example, when an action of `PostsController` is called Rails tries to load `posts_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases:
+For example, when an action of `ArticlesController` is called Rails tries to load `articles_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases:
```ruby
def default_helper_module!
@@ -3861,4 +3875,4 @@ rescue NameError => e
end
```
-NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`.
+NOTE: Defined in `active_support/core_ext/load_error.rb`.
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 6c77a40d42..7033947468 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -17,7 +17,7 @@ After reading this guide, you will know:
Introduction to instrumentation
-------------------------------
-The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in <TODO: link to section detailing each hook point>. With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code.
+The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code.
For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken.
@@ -364,7 +364,7 @@ INFO. Options passed to fetch will be merged with the payload.
| ------ | --------------------- |
| `:key` | Key used in the store |
-INFO. Cache stores my add their own keys
+INFO. Cache stores may add their own keys
```ruby
{
@@ -457,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 295c471db9..a2ebf55335 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -13,7 +13,19 @@ After reading this guide, you will know:
RDoc
----
-The Rails API documentation is generated with RDoc. Please consult the documentation for help with the [markup](http://rdoc.rubyforge.org/RDoc/Markup.html), and also take into account these [additional directives](http://rdoc.rubyforge.org/RDoc/Parser/Ruby.html).
+The [Rails API documentation](http://api.rubyonrails.org) is generated with
+[RDoc](http://docs.seattlerb.org/rdoc/).
+
+```bash
+ bundle exec rake rdoc
+```
+
+Resulting HTML files can be found in the ./doc/rdoc directory.
+
+Please consult the RDoc documentation for help with the
+[markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html),
+and also take into account these [additional
+directives](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html).
Wording
-------
@@ -67,7 +79,7 @@ used. Instead of:
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 (*color*, *center*, *modularize*, 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
------------
@@ -110,14 +122,14 @@ The results of expressions follow them and are introduced by "# => ", vertically
If a line is too long, the comment may be placed on the next line:
```ruby
-# label(:post, :title)
-# # => <label for="post_title">Title</label>
+# label(:article, :title)
+# # => <label for="article_title">Title</label>
#
-# label(:post, :title, "A short title")
-# # => <label for="post_title">A short title</label>
+# label(:article, :title, "A short title")
+# # => <label for="article_title">A short title</label>
#
-# label(:post, :title, "A short title", class: "title_label")
-# # => <label for="post_title" class="title_label">A short title</label>
+# label(:article, :title, "A short title", class: "title_label")
+# # => <label for="article_title" class="title_label">A short title</label>
```
Avoid using any printing methods like `puts` or `p` for that purpose.
@@ -175,8 +187,8 @@ end
The API is careful not to commit to any particular value, the method has
predicate semantics, that's enough.
-Filenames
----------
+File Names
+----------
As a rule of thumb, use filenames relative to the application root:
@@ -215,6 +227,13 @@ 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
When "true" and "false" are English words rather than Ruby keywords use a regular font:
@@ -279,3 +298,64 @@ self.class_eval %{
end
}
```
+
+Method Visibility
+-----------------
+
+When writing documentation for Rails, it's important to understand the difference between public user-facing API vs internal API.
+
+Rails, like most libraries, uses the private keyword from Ruby for defining internal API. However, public API follows a slightly different convention. Instead of assuming all public methods are designed for user consumption, Rails uses the `:nodoc:` directive to annotate these kinds of methods as internal API.
+
+This means that there are methods in Rails with `public` visibility that aren't meant for user consumption.
+
+An example of this is `ActiveRecord::Core::ClassMethods#arel_table`:
+
+```ruby
+module ActiveRecord::Core::ClassMethods
+ def arel_table #:nodoc:
+ # do some magic..
+ end
+end
+```
+
+If you thought, "this method looks like a public class method for `ActiveRecord::Core`", you were right. But actually the Rails team doesn't want users to rely on this method. So they mark it as `:nodoc:` and it's removed from public documentation. The reasoning behind this is to allow the team to change these methods according to their internal needs across releases as they see fit. The name of this method could change, or the return value, or this entire class may disappear; there's no guarantee and so you shouldn't depend on this API in your plugins or applications. Otherwise, you risk your app or gem breaking when you upgrade to a newer release of Rails.
+
+As a contributor, it's important to think about whether this API is meant for end-user consumption. The Rails team is committed to not making any breaking changes to public API across releases without going through a full deprecation cycle. It's recommended that you `:nodoc:` any of your internal methods/classes unless they're already private (meaning visibility), in which case it's internal by default. Once the API stabilizes the visibility can change, but changing public API is much harder due to backwards compatibility.
+
+A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly.
+
+If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project.
+
+A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails.
+
+To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first.
+
+Regarding the Rails Stack
+-------------------------
+
+When documenting parts of Rails API, it's important to remember all of the
+pieces that go into the Rails stack.
+
+This means that behavior may change depending on the scope or context of the
+method or class you're trying to document.
+
+In various places there is different behavior when you take the entire stack
+into account, one such example is
+`ActionView::Helpers::AssetTagHelper#image_tag`:
+
+```ruby
+# image_tag("icon.png")
+# # => <img alt="Icon" src="/assets/icon.png" />
+```
+
+Although the default behavior for `#image_tag` is to always return
+`/images/icon.png`, we take into account the full Rails stack (including the
+Asset Pipeline) we may see the result seen above.
+
+We're only concerned with the behavior experienced when using the full default
+Rails stack.
+
+In this case, we want to document the behavior of the _framework_, and not just
+this specific method.
+
+If you have a question on how the Rails team handles certain API, don't hesitate to open a ticket or send a patch to the [issue tracker](https://github.com/rails/rails/issues).
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 9ae3fbb0b5..e31cefa5bb 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -56,11 +56,11 @@ the comment operator on that line to later enable 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:
+`config.assets.js_compressor` for your JavaScript:
```ruby
config.assets.css_compressor = :yui
-config.assets.js_compressor = :uglify
+config.assets.js_compressor = :uglifier
```
NOTE: The `sass-rails` gem is automatically used for CSS compression if included
@@ -124,19 +124,22 @@ with a built-in helper. In the source the generated code looked like this:
The query string strategy has several disadvantages:
1. **Not all caches will reliably cache content where the filename only differs by
-query parameters**<br>
+query parameters**
+
[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>
+2. **The file name can change between nodes in multi-server environments.**
+
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>
+3. **Too much cache invalidation**
+
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.
@@ -198,12 +201,9 @@ 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.
+default .coffee and .scss files will not be precompiled on their own. 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
@@ -245,7 +245,7 @@ When a file is referenced from a manifest or a helper, Sprockets searches the
three default asset locations for it.
The default locations are: the `images`, `javascripts` and `stylesheets`
-directories under the `apps/assets` folder, but these subdirectories
+directories under the `app/assets` folder, but these subdirectories
are not special - any path under `assets/*` will be searched.
For example, these files:
@@ -302,7 +302,7 @@ 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
+`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.
@@ -493,14 +493,13 @@ The directives that work in JavaScript files also work in stylesheets
one, requiring all stylesheets from the current directory.
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.
+file (if any) at the precise location of the `require_self` call.
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
+instead of these Sprockets directives. When using Sprockets directives, 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.
+
+You can do file globbing as well using `@import "*"`, and `@import "**/*"` to add the whole tree which is equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats.
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
@@ -581,23 +580,21 @@ runtime. To disable this behavior you can set:
config.assets.raise_runtime_errors = false
```
-When `raise_runtime_errors` is set to `false` sprockets will not check that dependencies of assets are declared properly. Here is a scenario where you must tell the asset pipeline about a dependency:
-
-If you have `application.css.erb` that references `logo.png` like this:
+When this option is true, the asset pipeline will check if all the assets loaded
+in your application are included in the `config.assets.precompile` list.
+If `config.assets.digest` is also true, the asset pipeline will require that
+all requests for assets include digests.
-```css
-#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
-```
+### Turning Digests Off
-Then you must declare that `logo.png` is a dependency of `application.css.erb`, so when the image gets re-compiled, the css file does as well. You can do this using the `//= depend_on_asset` declaration:
+You can turn off digests by updating `config/environments/development.rb` to
+include:
-```css
-//= depend_on_asset "logo.png"
-#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
+```ruby
+config.assets.digest = false
```
-Without this declaration you may experience strange behavior when pushing to production that is difficult to debug. When you have `raise_runtime_errors` set to `true`, dependencies will be checked at runtime so you can ensure that all dependencies are met.
-
+When this option is true, digests will be generated for asset URLs.
### Turning Debugging Off
@@ -691,7 +688,7 @@ information on compiling locally.
The rake task is:
```bash
-$ RAILS_ENV=production bundle exec rake assets:precompile
+$ RAILS_ENV=production bin/rake assets:precompile
```
Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment.
@@ -714,7 +711,7 @@ The default matcher for compiling files includes `application.js`,
automatically) from `app/assets` folders including your gems:
```ruby
-[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
+[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) },
/application.(css|js)$/ ]
```
@@ -724,17 +721,17 @@ 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`:
+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:
```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
@@ -765,7 +762,7 @@ typical manifest file looks like:
"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":
+"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js":
"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
@@ -781,7 +778,7 @@ 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
+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.
@@ -793,13 +790,15 @@ For Apache:
# `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>
```
-For nginx:
+For NGINX:
```nginx
location ~ ^/assets/ {
@@ -821,7 +820,7 @@ 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`:
+NGINX is able to do this automatically enabling `gzip_static`:
```nginx
location ~ ^/(assets)/ {
@@ -840,7 +839,7 @@ the module compiled. Otherwise, you may need to perform a manual compilation:
./configure --with-http_gzip_static_module
```
-If you're compiling nginx with Phusion Passenger you'll need to pass that option
+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.
@@ -859,10 +858,12 @@ duplication of work.
Local compilation allows you to commit the compiled files into source control,
and deploy as normal.
-There are two caveats:
+There are three caveats:
* You must not run the Capistrano deployment task that precompiles assets.
-* You must change the following two application configuration settings.
+* You must ensure any necessary compressors or minifiers are
+available on your development system.
+* You must change the following application configuration setting:
In `config/environments/development.rb`, place the following line:
@@ -876,9 +877,6 @@ development mode, and pass all requests to Sprockets. The prefix is still set to
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 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.
@@ -925,9 +923,17 @@ cache forever. This can cause problems. If you use
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
+specific set up, you may not. The defaults NGINX uses, for example, should give
you no problems when used as an HTTP cache.
+If you want to serve only some assets from your CDN, you can use custom
+`:host` option of `asset_url` helper, which overwrites value set in
+`config.action_controller.asset_host`.
+
+```ruby
+asset_url 'image.png', :host => 'http://cdn.example.com'
+```
+
Customizing the Pipeline
------------------------
@@ -943,7 +949,7 @@ gem.
```ruby
config.assets.css_compressor = :yui
```
-The other option for compressing CSS if you have the sass-rails gem installed is
+The other option for compressing CSS if you have the sass-rails gem installed is
```ruby
config.assets.css_compressor = :sass
@@ -1018,14 +1024,15 @@ 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.
+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
+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
+# 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
@@ -1033,6 +1040,10 @@ 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
------------------
@@ -1051,6 +1062,14 @@ cache store.
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
--------------------------
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 5ec6ae0f21..daf4113b66 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -105,7 +105,7 @@ class CreateOrders < ActiveRecord::Migration
end
create_table :orders do |t|
- t.belongs_to :customer
+ t.belongs_to :customer, index: true
t.datetime :order_date
t.timestamps
end
@@ -136,7 +136,7 @@ class CreateSuppliers < ActiveRecord::Migration
end
create_table :accounts do |t|
- t.belongs_to :supplier
+ t.belongs_to :supplier, index: true
t.string :account_number
t.timestamps
end
@@ -169,7 +169,7 @@ class CreateCustomers < ActiveRecord::Migration
end
create_table :orders do |t|
- t.belongs_to :customer
+ t.belongs_to :customer, index:true
t.datetime :order_date
t.timestamps
end
@@ -216,8 +216,8 @@ class CreateAppointments < ActiveRecord::Migration
end
create_table :appointments do |t|
- t.belongs_to :physician
- t.belongs_to :patient
+ t.belongs_to :physician, index: true
+ t.belongs_to :patient, index: true
t.datetime :appointment_date
t.timestamps
end
@@ -295,13 +295,13 @@ class CreateAccountHistories < ActiveRecord::Migration
end
create_table :accounts do |t|
- t.belongs_to :supplier
+ t.belongs_to :supplier, index: true
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
- t.belongs_to :account
+ t.belongs_to :account, index: true
t.integer :credit_rating
t.timestamps
end
@@ -341,8 +341,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration
end
create_table :assemblies_parts, id: false do |t|
- t.belongs_to :assembly
- t.belongs_to :part
+ t.belongs_to :assembly, index: true
+ t.belongs_to :part, index: true
end
end
end
@@ -379,6 +379,8 @@ class CreateSuppliers < ActiveRecord::Migration
t.string :account_number
t.timestamps
end
+
+ add_index :accounts, :supplier_id
end
end
```
@@ -455,6 +457,8 @@ class CreatePictures < ActiveRecord::Migration
t.string :imageable_type
t.timestamps
end
+
+ add_index :pictures, :imageable_id
end
end
```
@@ -466,7 +470,7 @@ class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
- t.references :imageable, polymorphic: true
+ t.references :imageable, polymorphic: true, index: true
t.timestamps
end
end
@@ -496,7 +500,7 @@ In your migrations/schema, you will add a references column to the model itself.
class CreateEmployees < ActiveRecord::Migration
def change
create_table :employees do |t|
- t.references :manager
+ t.references :manager, index: true
t.timestamps
end
end
@@ -561,6 +565,8 @@ class CreateOrders < ActiveRecord::Migration
t.string :order_number
t.integer :customer_id
end
+
+ add_index :orders, :customer_id
end
end
```
@@ -571,7 +577,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:
@@ -594,6 +600,9 @@ class CreateAssembliesPartsJoinTable < ActiveRecord::Migration
t.integer :assembly_id
t.integer :part_id
end
+
+ add_index :assemblies_parts, :assembly_id
+ add_index :assemblies_parts, :part_id
end
end
```
@@ -1131,7 +1140,7 @@ The `has_one` association supports these options:
##### `:as`
-Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>.
+Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail [earlier in this guide](#polymorphic-associations).
##### `:autosave`
@@ -1203,7 +1212,7 @@ The `:source_type` option specifies the source association type for a `has_one :
##### `:through`
-The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail <a href="#the-has-one-through-association">earlier in this guide</a>.
+The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail [earlier in this guide](#the-has-one-through-association).
##### `:validate`
@@ -1497,7 +1506,7 @@ The `has_many` association supports these options:
##### `:as`
-Setting the `:as` option indicates that this is a polymorphic association, as discussed <a href="#polymorphic-associations">earlier in this guide</a>.
+Setting the `:as` option indicates that this is a polymorphic association, as discussed [earlier in this guide](#polymorphic-associations).
##### `:autosave`
@@ -1579,7 +1588,7 @@ The `:source_type` option specifies the source association type for a `has_many
##### `:through`
-The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has-many-through-association">earlier in this guide</a>.
+The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed [earlier in this guide](#the-has-many-through-association).
##### `:validate`
@@ -1632,7 +1641,7 @@ If you use a hash-style `where` option, then record creation via this associatio
##### `extending`
-The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>.
+The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions).
##### `group`
@@ -1725,58 +1734,58 @@ mostly useful together with the `:through` option.
```ruby
class Person < ActiveRecord::Base
has_many :readings
- has_many :posts, through: :readings
+ has_many :articles, through: :readings
end
person = Person.create(name: 'John')
-post = Post.create(name: 'a1')
-person.posts << post
-person.posts << post
-person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">]
-Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>]
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
+Reading.all.inspect # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
```
-In the above case there are two readings and `person.posts` brings out both of
-them even though these records are pointing to the same post.
+In the above case there are two readings and `person.articles` brings out both of
+them even though these records are pointing to the same article.
Now let's set `distinct`:
```ruby
class Person
has_many :readings
- has_many :posts, -> { distinct }, through: :readings
+ has_many :articles, -> { distinct }, through: :readings
end
person = Person.create(name: 'Honda')
-post = Post.create(name: 'a1')
-person.posts << post
-person.posts << post
-person.posts.inspect # => [#<Post id: 7, name: "a1">]
-Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>]
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 7, name: "a1">]
+Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
```
-In the above case there are still two readings. However `person.posts` shows
-only one post because the collection loads only unique records.
+In the above case there are still two readings. However `person.articles` shows
+only one article because the collection loads only unique records.
If you want to make sure that, upon insertion, all of the records in the
persisted association are distinct (so that you can be sure that when you
inspect the association that you will never find duplicate records), you should
add a unique index on the table itself. For example, if you have a table named
-`person_posts` and you want to make sure all the posts are unique, you could
+`person_articles` and you want to make sure all the articles are unique, you could
add the following in a migration:
```ruby
-add_index :person_posts, :post, unique: true
+add_index :person_articles, :article, unique: true
```
Note that checking for uniqueness using something like `include?` is subject
to race conditions. Do not attempt to use `include?` to enforce distinctness
-in an association. For instance, using the post example from above, the
+in an association. For instance, using the article example from above, the
following code would be racy because multiple users could be attempting this
at the same time:
```ruby
-person.posts << post unless person.posts.include?(post)
+person.articles << article unless person.articles.include?(article)
```
#### When are Objects Saved?
@@ -2082,7 +2091,7 @@ If you use a hash-style `where`, then record creation via this association will
##### `extending`
-The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>.
+The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions).
##### `group`
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 0d45e5fb28..0902e347e2 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -28,15 +28,15 @@ config.action_controller.perform_caching = true
### Page Caching
-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.
+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
@@ -105,7 +105,7 @@ This method generates a cache key that depends on all products and can be used i
<% end %>
```
-If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless`
+If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless`
```erb
<% cache_if (condition, cache_key_for_products) do %>
@@ -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.
@@ -165,7 +185,7 @@ end
Cache Stores
------------
-Rails provides different stores for the cached data created by <b>action</b> and <b>fragment</b> caches.
+Rails provides different stores for the cached data created by **action** and **fragment** caches.
TIP: Page caches are always stored on disk.
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 3b80faec7f..a074b849c6 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -7,7 +7,6 @@ After reading this guide, you will know:
* How to generate models, controllers, database migrations, and unit tests.
* How to start a development server.
* How to experiment with objects through an interactive shell.
-* How to profile and benchmark your new creation.
--------------------------------------------------------------------------------
@@ -60,9 +59,9 @@ With no further work, `rails server` will run our new shiny Rails app:
```bash
$ cd commandsapp
-$ rails server
+$ bin/rails server
=> Booting WEBrick
-=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000
+=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2013-08-07 02:00:01] INFO WEBrick 1.3.1
@@ -77,7 +76,7 @@ INFO: You can also use the alias "s" to start the server: `rails s`.
The server can be run on a different port using the `-p` option. The default development environment can be changed using `-e`.
```bash
-$ rails server -e production -p 4000
+$ bin/rails server -e production -p 4000
```
The `-b` option binds Rails to the specified IP, by default it is 0.0.0.0. You can run a server as a daemon by passing a `-d` option.
@@ -89,7 +88,7 @@ The `rails generate` command uses templates to create a whole lot of things. Run
INFO: You can also use the alias "g" to invoke the generator command: `rails g`.
```bash
-$ rails generate
+$ bin/rails generate
Usage: rails generate GENERATOR [args] [options]
...
@@ -114,7 +113,7 @@ Let's make our own controller with the controller generator. But what command sh
INFO: All Rails console utilities have help text. As with most *nix utilities, you can try adding `--help` or `-h` to the end, for example `rails server --help`.
```bash
-$ rails generate controller
+$ bin/rails generate controller
Usage: rails generate controller NAME [action action] [options]
...
@@ -123,25 +122,24 @@ Usage: rails generate controller NAME [action action] [options]
Description:
...
- To create a controller within a module, specify the controller name as a
- path like 'parent_module/controller_name'.
+ To create a controller within a module, specify the controller name as a path like 'parent_module/controller_name'.
...
Example:
- `rails generate controller CreditCard open debit credit close`
+ `rails generate controller CreditCards open debit credit close`
- Credit card controller with URLs like /credit_card/debit.
+ Credit card controller with URLs like /credit_cards/debit.
Controller: app/controllers/credit_card_controller.rb
- Test: test/controllers/credit_card_controller_test.rb
- Views: app/views/credit_card/debit.html.erb [...]
- Helper: app/helpers/credit_card_helper.rb
+ Test: test/controllers/credit_cards_controller_test.rb
+ Views: app/views/credit_cards/debit.html.erb [...]
+ Helper: app/helpers/credit_cards_helper.rb
```
The controller generator is expecting parameters in the form of `generate controller ControllerName action1 action2`. Let's make a `Greetings` controller with an action of **hello**, which will say something nice to us.
```bash
-$ rails generate controller Greetings hello
+$ bin/rails generate controller Greetings hello
create app/controllers/greetings_controller.rb
route get "greetings/hello"
invoke erb
@@ -151,8 +149,6 @@ $ rails generate controller Greetings hello
create test/controllers/greetings_controller_test.rb
invoke helper
create app/helpers/greetings_helper.rb
- invoke test_unit
- create test/helpers/greetings_helper_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/greetings.js.coffee
@@ -182,7 +178,7 @@ Then the view, to display our message (in `app/views/greetings/hello.html.erb`):
Fire up your server using `rails server`.
```bash
-$ rails server
+$ bin/rails server
=> Booting WEBrick...
```
@@ -193,7 +189,7 @@ INFO: With a normal, plain-old Rails application, your URLs will generally follo
Rails comes with a generator for data models too.
```bash
-$ rails generate model
+$ bin/rails generate model
Usage:
rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
@@ -216,7 +212,7 @@ But instead of generating a model directly (which we'll be doing later), let's s
We will set up a simple resource called "HighScore" that will keep track of our highest score on video games we play.
```bash
-$ rails generate scaffold HighScore game:string score:integer
+$ bin/rails generate scaffold HighScore game:string score:integer
invoke active_record
create db/migrate/20130717151933_create_high_scores.rb
create app/models/high_score.rb
@@ -238,8 +234,6 @@ $ rails generate scaffold HighScore game:string score:integer
create test/controllers/high_scores_controller_test.rb
invoke helper
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
@@ -254,10 +248,10 @@ $ rails generate scaffold HighScore game:string score:integer
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 `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.
+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
+$ bin/rake db:migrate
== CreateHighScores: migrating ===============================================
-- create_table(:high_scores)
-> 0.0017s
@@ -269,7 +263,7 @@ INFO: Let's talk about unit tests. Unit tests are code that tests and makes asse
Let's see the interface Rails created for us.
```bash
-$ rails server
+$ bin/rails server
```
Go to your browser and open [http://localhost:3000/high_scores](http://localhost:3000/high_scores), now we can create new high scores (55,160 on Space Invaders!)
@@ -283,18 +277,43 @@ INFO: You can also use the alias "c" to invoke the console: `rails c`.
You can specify the environment in which the `console` command should operate.
```bash
-$ rails console staging
+$ bin/rails console staging
```
If you wish to test out some code without changing any data, you can do that by invoking `rails console --sandbox`.
```bash
-$ rails console --sandbox
-Loading development environment in sandbox (Rails 4.0.0)
+$ bin/rails console --sandbox
+Loading development environment in sandbox (Rails 4.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0>
```
+#### The app and helper objects
+
+Inside the `rails console` you have access to the `app` and `helper` instances.
+
+With the `app` method you can access url and path helpers, as well as do requests.
+
+```bash
+>> app.root_path
+=> "/"
+
+>> app.get _
+Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300
+...
+```
+
+With the `helper` method it is possible to access Rails and your application's helpers.
+
+```bash
+>> helper.time_ago_in_words 30.days.ago
+=> "about 1 month"
+
+>> helper.my_custom_helper
+=> "my custom helper"
+```
+
### `rails dbconsole`
`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3.
@@ -306,7 +325,7 @@ INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`.
`runner` runs Ruby code in the context of Rails non-interactively. For instance:
```bash
-$ rails runner "Model.long_running_method"
+$ bin/rails runner "Model.long_running_method"
```
INFO: You can also use the alias "r" to invoke the runner: `rails r`.
@@ -314,7 +333,7 @@ INFO: You can also use the alias "r" to invoke the runner: `rails r`.
You can specify the environment in which the `runner` command should operate using the `-e` switch.
```bash
-$ rails runner -e staging "Model.long_running_method"
+$ bin/rails runner -e staging "Model.long_running_method"
```
### `rails destroy`
@@ -324,7 +343,7 @@ Think of `destroy` as the opposite of `generate`. It'll figure out what generate
INFO: You can also use the alias "d" to invoke the destroy command: `rails d`.
```bash
-$ rails generate model Oops
+$ bin/rails generate model Oops
invoke active_record
create db/migrate/20120528062523_create_oops.rb
create app/models/oops.rb
@@ -333,7 +352,7 @@ $ rails generate model Oops
create test/fixtures/oops.yml
```
```bash
-$ rails destroy model Oops
+$ bin/rails destroy model Oops
invoke active_record
remove db/migrate/20120528062523_create_oops.rb
remove app/models/oops.rb
@@ -353,9 +372,10 @@ To get the full backtrace for running rake task you can pass the option
```--trace``` to command line, for example ```rake db:create --trace```.
```bash
-$ rake --tasks
+$ bin/rake --tasks
rake about # List versions of all Rails frameworks and the environment
-rake assets:clean # Remove compiled assets
+rake assets:clean # Remove old compiled assets
+rake assets:clobber # Remove compiled assets
rake assets:precompile # Compile all the assets named in config.assets.precompile
rake db:create # Create the database from config/database.yml for the current Rails.env
...
@@ -372,18 +392,18 @@ INFO: You can also use ```rake -T``` to get the list of tasks.
`rake about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation.
```bash
-$ rake about
+$ bin/rake about
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.1.0
+Rails version 4.2.0
JavaScript Runtime Node.js (V8)
-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
+Active Record version 4.2.0
+Action Pack version 4.2.0
+Action View version 4.2.0
+Action Mailer version 4.2.0
+Active Support version 4.2.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
@@ -393,7 +413,12 @@ Database schema version 20110805173523
### `assets`
-You can precompile the assets in `app/assets` using `rake assets:precompile` and remove those compiled assets using `rake assets:clean`.
+You can precompile the assets in `app/assets` using `rake assets:precompile`,
+and remove older compiled assets using `rake assets:clean`. The `assets:clean`
+task allows for rolling deploys that may still be linking to an old asset while
+the new assets are being built.
+
+If you want to clear `public/assets` completely, you can use `rake assets:clobber`.
### `db`
@@ -411,10 +436,10 @@ 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
+$ bin/rake notes
(in /home/foobar/commandsapp)
app/controllers/admin/users_controller.rb:
* [ 20] [TODO] any other way to do this?
@@ -425,10 +450,16 @@ 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
-$ rake notes:fixme
+$ bin/rake notes:fixme
(in /home/foobar/commandsapp)
app/controllers/admin/users_controller.rb:
* [132] high priority for next deploy
@@ -440,9 +471,9 @@ app/models/school.rb:
You can also use custom annotations in your code and list them using `rake notes:custom` by specifying the annotation using an environment variable `ANNOTATION`.
```bash
-$ rake notes:custom ANNOTATION=BUG
+$ bin/rake notes:custom ANNOTATION=BUG
(in /home/foobar/commandsapp)
-app/models/post.rb:
+app/models/article.rb:
* [ 23] Have to fix this one before pushing!
```
@@ -452,7 +483,7 @@ By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `tes
```bash
$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
-$ rake notes
+$ bin/rake notes
(in /home/foobar/commandsapp)
app/models/user.rb:
* [ 35] [FIXME] User should have a subscription at this point
@@ -524,9 +555,9 @@ end
Invocation of the tasks will look like:
```bash
-rake task_name
-rake "task_name[value 1]" # entire argument string should be quoted
-rake db:nothing
+$ bin/rake task_name
+$ bin/rake "task_name[value 1]" # entire argument string should be quoted
+$ bin/rake db:nothing
```
NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the `environment` task, which will load your application code.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 7b72e27b96..801cef5ca6 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.
@@ -110,7 +110,7 @@ numbers. New applications filter out passwords by adding the following `config.f
* `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.
@@ -118,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:
@@ -137,7 +137,7 @@ numbers. New applications filter out passwords by adding the following `config.f
* `config.assets.enabled` a flag that controls whether the asset
pipeline is enabled. It is set to true by default.
-*`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.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`.
@@ -151,6 +151,8 @@ pipeline is enabled. It is set to true by default.
* `config.assets.prefix` defines the prefix where assets are served from. Defaults to `/assets`.
+* `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder.
+
* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb`.
* `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`.
@@ -274,7 +276,7 @@ All these configuration options are delegated to the `I18n` library.
* `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.
@@ -330,6 +332,8 @@ The schema dumper adds one additional configuration option:
* `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.
+* `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`.
+
### Configuring Action Dispatch
* `config.action_dispatch.session_store` sets the name of the store for session data. The default is `:cookie_store`; other valid options include `:active_record_store`, `:mem_cache_store` or the name of your own custom class.
@@ -388,13 +392,13 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
* `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`
-* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::PostsController` which renders this template:
+* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template:
```erb
- <%= render @post %>
+ <%= render @article %>
```
- 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`.
+ The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`.
* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations
@@ -451,6 +455,18 @@ There are a number of settings available on `config.action_mailer`:
config.action_mailer.interceptors = ["MailInterceptor"]
```
+* `config.action_mailer.preview_path` specifies the location of mailer previews.
+
+ ```ruby
+ config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+ ```
+
+* `config.action_mailer.show_previews` enable or disable mailer previews. By default this is `true` in development.
+
+ ```ruby
+ config.action_mailer.show_previews = false
+ ```
+
### Configuring Active Support
There are a few configuration options available in Active Support:
@@ -552,7 +568,7 @@ development:
$ echo $DATABASE_URL
postgresql://localhost/my_database
-$ rails runner 'puts ActiveRecord::Base.connections'
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
```
@@ -569,7 +585,7 @@ development:
$ echo $DATABASE_URL
postgresql://localhost/my_database
-$ rails runner 'puts ActiveRecord::Base.connections'
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}}
```
@@ -580,13 +596,13 @@ The only way to explicitly not use the connection information in `ENV['DATABASE_
```
$ cat config/database.yml
development:
- url: sqlite3://localhost/NOT_my_database
+ url: sqlite3:NOT_my_database
$ echo $DATABASE_URL
postgresql://localhost/my_database
-$ rails runner 'puts ActiveRecord::Base.connections'
-{"development"=>{"adapter"=>"sqlite3", "host"=>"localhost", "database"=>"NOT_my_database"}}
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
```
Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name.
@@ -644,11 +660,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:
@@ -656,6 +670,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:
@@ -721,13 +745,47 @@ 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).
+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
+Deploying your application using a reverse proxy has definite advantages over traditional deploys. They allow you to have more control over your server by layering the components required by your application.
+
+Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers.
+
+One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy.
+
+In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead.
+
+You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it.
+
+Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include:
+
+```
+upstream application_server {
+ server 0.0.0.0:8080
+}
+
+server {
+ listen 80;
+ server_name localhost;
+
+ root /root/path/to/your_app/public;
+
+ try_files $uri/index.html $uri.html @app;
+
+ location @app {
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_redirect off;
+ proxy_pass http://application_server;
+ }
+
+ # some other configuration
+}
+```
+
+Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information.
#### Considerations when deploying to a subdirectory
@@ -939,4 +997,25 @@ ActiveRecord::ConnectionTimeoutError - could not obtain a database connection wi
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. As Rails is multi-threaded by default, 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.
+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.
+
+
+Custom configuration
+--------------------
+
+You can configure your own code through the Rails configuration object with custom configuration. It works like this:
+
+ ```ruby
+ config.x.payment_processing.schedule = :daily
+ config.x.payment_processing.retries = 3
+ config.x.super_debugger = true
+ ```
+
+These configuration points are then available through the configuration object:
+
+ ```ruby
+ Rails.configuration.x.payment_processing.schedule # => :daily
+ Rails.configuration.x.payment_processing.retries # => 3
+ Rails.configuration.x.super_debugger # => true
+ Rails.configuration.x.super_debugger.not_set # => nil
+ ```
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 36e3862c6b..0b05725623 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -24,9 +24,9 @@ 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.
@@ -50,7 +50,7 @@ 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
+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.
@@ -69,89 +69,6 @@ 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.
-
-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
----------------------------------
@@ -192,9 +109,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.
-</blockquote>
+>I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too.
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.
@@ -227,9 +142,21 @@ WARNING: Docrails has a very strict policy: no code can be touched whatsoever, n
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
@@ -244,29 +171,31 @@ $ 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.
+### 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
-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.
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:
@@ -284,13 +213,136 @@ Rails follows a simple set of coding style conventions:
The above are guidelines - please use your best judgment in using them.
+### Benchmark Your Code
+
+If your change has an impact on the performance of Rails, please use the
+[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide
+benchmark results for comparison.
+
+Here's an example of using benchmark-ips:
+
+```ruby
+require 'benchmark/ips'
+
+Benchmark.ips do |x|
+ x.report('addition') { 1 + 2 }
+ x.report('addition with send') { 1.send(:+, 2) }
+end
+```
+
+This will generate a report with the following information:
+
+```
+Calculating -------------------------------------
+ addition 69114 i/100ms
+ addition with send 64062 i/100ms
+-------------------------------------------------
+ addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s
+ addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s
+```
+
+Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information.
+
+### 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
+```
+
+To run a single test against all adapters, use:
+
+```bash
+$ bundle exec rake TEST=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
The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version.
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 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 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
@@ -314,9 +366,9 @@ Your name can be added directly after the last word if you don't provide any cod
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
+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
@@ -343,9 +395,9 @@ it should not be necessary to visit a webpage to check the history.
Description can have multiple paragraphs and you can use code examples
inside, just indent it with 4 spaces:
- class PostsController
+ class ArticlesController
def index
- respond_with Post.limit(10)
+ respond_with Article.limit(10)
end
end
@@ -467,11 +519,11 @@ 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
@@ -507,7 +559,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 4-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 7c6858fa2c..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 %>
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index 226137c89a..53b8566d83 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -26,17 +26,17 @@ One common task is to inspect the contents of a variable. In Rails, you can do t
The `debug` helper will return a \<pre> tag that renders the object using the YAML format. This will generate human-readable data from any object. For example, if you have this code in a view:
```html+erb
-<%= debug @post %>
+<%= debug @article %>
<p>
<b>Title:</b>
- <%= @post.title %>
+ <%= @article.title %>
</p>
```
You'll see something like this:
```yaml
---- !ruby/object:Post
+--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: It's a very helpful guide for debugging your Rails app.
@@ -55,10 +55,10 @@ Title: Rails debugging guide
Displaying an instance variable, or any other object or method, in YAML format can be achieved this way:
```html+erb
-<%= simple_format @post.to_yaml %>
+<%= simple_format @article.to_yaml %>
<p>
<b>Title:</b>
- <%= @post.title %>
+ <%= @article.title %>
</p>
```
@@ -67,7 +67,7 @@ The `to_yaml` method converts the method to YAML format leaving it more readable
As a result of this, you will have something like this in your view:
```yaml
---- !ruby/object:Post
+--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: It's a very helpful guide for debugging your Rails app.
@@ -88,7 +88,7 @@ Another useful method for displaying object values is `inspect`, especially when
<%= [1, 2, 3, 4, 5].inspect %>
<p>
<b>Title:</b>
- <%= @post.title %>
+ <%= @article.title %>
</p>
```
@@ -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
@@ -153,18 +153,18 @@ logger.fatal "Terminating application, raised unrecoverable error!!!"
Here's an example of a method instrumented with extra logging:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
# ...
def create
- @post = Post.new(params[:post])
- logger.debug "New post: #{@post.attributes.inspect}"
- logger.debug "Post should be valid: #{@post.valid?}"
-
- if @post.save
- flash[:notice] = 'Post was successfully created.'
- logger.debug "The post was saved and now the user is going to be redirected..."
- redirect_to(@post)
+ @article = Article.new(params[:article])
+ logger.debug "New article: #{@article.attributes.inspect}"
+ logger.debug "Article should be valid: #{@article.valid?}"
+
+ if @article.save
+ flash[:notice] = 'Article was successfully created.'
+ logger.debug "The article was saved and now the user is going to be redirected..."
+ redirect_to(@article)
else
render action: "new"
end
@@ -177,21 +177,21 @@ end
Here's an example of the log generated when this controller action is executed:
```
-Processing PostsController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST]
+Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST]
Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl
vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4
- Parameters: {"commit"=>"Create", "post"=>{"title"=>"Debugging Rails",
+ Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails",
"body"=>"I'm learning how to print in logs!!!", "published"=>"0"},
- "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"posts"}
-New post: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!",
+ "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"}
+New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!",
"published"=>false, "created_at"=>nil}
-Post should be valid: true
- Post Create (0.000443) INSERT INTO "posts" ("updated_at", "title", "body", "published",
+Article should be valid: true
+ Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published",
"created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails',
'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54')
-The post was saved and now the user is going to be redirected...
-Redirected to #<Post:0x20af760>
-Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts]
+The article was saved and now the user is going to be redirected...
+Redirected to # Article:0x20af760>
+Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/articles]
```
Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia.
@@ -210,7 +210,7 @@ logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "
```
### Impact of Logs on Performance
-Logging will always have a small impact on performance of your rails app,
+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`,
@@ -224,446 +224,576 @@ Another potential pitfall is that if you have many calls to `Logger` like this
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
+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
+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
+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 `debugger` gem
+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/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
+
+(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.2.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 ArticlesController#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/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
-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
-[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) l-
-If you repeat the `list` command, this time using just `l`, the next ten lines of the file will be printed out.
+[1, 10] in /PathTo/project/app/controllers/articles_controller.rb
+ 1 class ArticlesController < ApplicationController
+ 2 before_action :set_article, only: [:show, :edit, :update, :destroy]
+ 3
+ 4 # GET /articles
+ 5 # GET /articles.json
+ 6 def index
+ 7 byebug
+ 8 @articles = Article.find_recent
+ 9
+ 10 respond_to do |format|
```
-(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.
-On the other hand, to see the previous ten lines you should type `list-` (or `l-`)
+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=`
```
-(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/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
-```
-(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 ArticlesController.index
+ at /PathTo/project/test_app/app/controllers/articles_controller.rb:8
+ #1 ActionController::ImplicitRender.send_action(method#String, *args#Array)
+ at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/implicit_render.rb:4
+ #2 AbstractController::Base.process_action(action#NilClass, *args#Array)
+ at /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb:189
+ #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass)
+ at /PathToGems/actionpack-4.2.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.2.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/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
+
+(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/articles_controller.rb
+ 5 # GET /articles.json
+ 6 def index
+ 7 byebug
+ 8 @articles = Article.find_recent
+ 9
+=> 10 respond_to do |format|
+ 11 format.html # index.html.erb
+ 12 format.json { render json: @articles }
+ 13 end
+ 14 end
+ 15
+(byebug)
```
And then ask again for the instance_variables:
```
-(rdb:11) instance_variables.include? "@posts"
+(byebug) instance_variables.include? "@articles"
true
```
-Now `@posts` is included in the instance variables, because the line defining it was executed.
+Now `@articles` 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 Article.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 @articles
+1: @articles = 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 ArticlesController#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/article.rb
+ 1: class Article < 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/articles_controller.rb
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: @articles = Article.find_recent
+ 8:
+=> 9: respond_to do |format|
+ 10: format.html # index.html.erb
+ 11: format.json { render json: @articles }
+ 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.2.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
+(byebug)
```
-(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 = []
-```
-
-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/articles_controller.rb
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: @articles = Article.find_recent
+ 8:
+=> 9: respond_to do |format|
+ 10: format.html # index.html.erb
+ 11: format.json { render json: @articles }
+ 12: end
+ 13: end
+
+(byebug) break 11
+Created breakpoint 1 at /PathTo/project/app/controllers/articles_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/articles_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:
-
-* `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
+`byebug` has a few available options to tweak its behaviour:
-You can see the full list by using `help set`. Use `help set _subcommand_` to learn about a particular `set` command.
+* `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.
-TIP: You can save these settings in an `.rdebugrc` file in your home directory. The debugger reads these global settings when it starts.
+You can see the full list by using `help set`. Use `help set _subcommand_` to
+learn about a particular `set` command.
-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
```
@@ -671,35 +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, 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.
+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.
-* [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.
+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 4ee43b6a97..b134c9d2d0 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -117,7 +117,7 @@ This command will install all dependencies except the MySQL and PostgreSQL Ruby
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 to install memcached on OSX:
+You can use [Homebrew](http://brew.sh/) to install memcached on OSX:
```bash
$ brew install memcached
@@ -210,6 +210,14 @@ FreeBSD users will have to run the following:
# 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).
@@ -241,20 +249,25 @@ and create the test databases:
```bash
$ cd activerecord
-$ bundle exec rake mysql:build_databases
+$ bundle exec rake db:mysql:build
```
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
```bash
$ cd activerecord
-$ bundle exec rake postgresql:build_databases
+$ bundle exec rake db:postgresql:build
```
It is possible to build databases for both PostgreSQL and MySQL with
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index e4653b47fc..82e248ee38 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -13,8 +13,8 @@
url: active_record_basics.html
description: This guide will get you started with models, persistence to database and the Active Record pattern and library.
-
- name: Rails Database Migrations
- url: migrations.html
+ name: Active Record Migrations
+ url: active_record_migrations.html
description: This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner.
-
name: Active Record Validations
@@ -96,11 +96,6 @@
url: command_line.html
description: This guide covers the command line tools and rake tasks provided by Rails.
-
- name: Caching with Rails
- work_in_progress: true
- url: caching_with_rails.html
- description: Various caching techniques provided by Rails.
- -
name: Asset Pipeline
url: asset_pipeline.html
description: This guide documents the asset pipeline.
@@ -162,12 +157,10 @@
-
name: Upgrading Ruby on Rails
url: upgrading_ruby_on_rails.html
- 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
- work_in_progress: true
description: Release notes for Rails 4.1.
-
name: Ruby on Rails 4.0 Release Notes
diff --git a/guides/source/engines.md b/guides/source/engines.md
index bbd63bb892..24548a5b01 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -31,27 +31,28 @@ 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.
+command). We'll actually be using the `--mountable` option here, which includes
+all the features of `--full`, and then some. This guide will refer to these
+"full plugins" 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
+The engine that will be created in this guide will be called "blorgh". This
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
+for new articles 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
+`articles_path` and use an engine also that provides a path also called
+`articles_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.
+has final say in what goes on in its environment. 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
@@ -73,17 +74,20 @@ 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
+$ bin/rails plugin new blorgh --mountable
```
The full list of options for the plugin generator may be seen by typing:
```bash
-$ rails plugin --help
+$ bin/rails plugin --help
```
-The `--full` option tells the generator that you want to create an engine,
-including a skeleton structure that provides the following:
+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. 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:
@@ -94,7 +98,7 @@ including a skeleton structure that provides the following:
```
* A file at `lib/blorgh/engine.rb`, which is identical in function to a
- * standard Rails application's `config/application.rb` file:
+ standard Rails application's `config/application.rb` file:
```ruby
module Blorgh
@@ -103,9 +107,7 @@ including a skeleton structure that provides the following:
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 will add to the `--full` option:
* Asset manifest files (`application.js` and `application.css`)
* A namespaced `ApplicationController` stub
@@ -134,7 +136,7 @@ following to the dummy application's routes file at
`test/dummy/config/routes.rb`:
```ruby
-mount Blorgh::Engine, at: "blorgh"
+mount Blorgh::Engine => "/blorgh"
```
### Inside an Engine
@@ -171,7 +173,7 @@ Within `lib/blorgh/engine.rb` is the base class for the engine:
```ruby
module Blorgh
- class Engine < Rails::Engine
+ class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
@@ -197,12 +199,12 @@ 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
+to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but
+instead be namespaced and called `Blorgh::Article`. In addition, the table for the
+model is namespaced, becoming `blorgh_articles`, rather than simply `articles`.
+Similar to the model namespacing, a controller called `ArticlesController` becomes
+`Blorgh::ArticlesController` and the views for that controller will not be at
+`app/views/articles`, but `app/views/blorgh/articles` instead. Mailers are namespaced
as well.
Finally, routes will also be isolated within the engine. This is one of the most
@@ -253,7 +255,7 @@ 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
+$ bin/rails g model
```
Keep in mind, of course, that anything generated with these commands inside of
@@ -283,74 +285,72 @@ created in the `test` directory as well. For example, you may wish to create a
Providing engine functionality
------------------------------
-The engine that this guide covers provides posting and commenting functionality
-and follows a similar thread to the [Getting Started
+The engine that this guide covers provides submitting articles 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 an Article Resource
-The first thing to generate for a blog engine is the `Post` model and related
+The first thing to generate for a blog engine is the `Article` model and related
controller. To quickly generate this, you can use the Rails scaffold generator.
```bash
-$ rails generate scaffold post title:string text:text
+$ bin/rails generate scaffold article title:string text:text
```
This command will output this information:
```
invoke active_record
-create db/migrate/[timestamp]_create_blorgh_posts.rb
-create app/models/blorgh/post.rb
+create db/migrate/[timestamp]_create_blorgh_articles.rb
+create app/models/blorgh/article.rb
invoke test_unit
-create test/models/blorgh/post_test.rb
-create test/fixtures/blorgh/posts.yml
+create test/models/blorgh/article_test.rb
+create test/fixtures/blorgh/articles.yml
invoke resource_route
- route resources :posts
+ route resources :articles
invoke scaffold_controller
-create app/controllers/blorgh/posts_controller.rb
+create app/controllers/blorgh/articles_controller.rb
invoke erb
-create app/views/blorgh/posts
-create app/views/blorgh/posts/index.html.erb
-create app/views/blorgh/posts/edit.html.erb
-create app/views/blorgh/posts/show.html.erb
-create app/views/blorgh/posts/new.html.erb
-create app/views/blorgh/posts/_form.html.erb
+create app/views/blorgh/articles
+create app/views/blorgh/articles/index.html.erb
+create app/views/blorgh/articles/edit.html.erb
+create app/views/blorgh/articles/show.html.erb
+create app/views/blorgh/articles/new.html.erb
+create app/views/blorgh/articles/_form.html.erb
invoke test_unit
-create test/controllers/blorgh/posts_controller_test.rb
+create test/controllers/blorgh/articles_controller_test.rb
invoke helper
-create app/helpers/blorgh/posts_helper.rb
-invoke test_unit
-create test/helpers/blorgh/posts_helper_test.rb
+create app/helpers/blorgh/articles_helper.rb
invoke assets
invoke js
-create app/assets/javascripts/blorgh/posts.js
+create app/assets/javascripts/blorgh/articles.js
invoke css
-create app/assets/stylesheets/blorgh/posts.css
+create app/assets/stylesheets/blorgh/articles.css
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
+however, that the migration is called `create_blorgh_articles` rather than the
+usual `create_articles`. 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
+being placed at `app/models/blorgh/article.rb` rather than `app/models/article.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`).
+test at `test/models/blorgh/article_test.rb` (rather than
+`test/models/article_test.rb`) and a fixture at `test/fixtures/blorgh/articles.yml`
+(rather than `test/fixtures/articles.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
+for the engine. This line is simply `resources :articles`, turning the
`config/routes.rb` file for the engine into this:
```ruby
Blorgh::Engine.routes.draw do
- resources :posts
+ resources :articles
end
```
@@ -362,18 +362,18 @@ 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`).
+called `Blorgh::ArticlesController` (at
+`app/controllers/blorgh/articles_controller.rb`) and its related views at
+`app/views/blorgh/articles`. This generator also generates a test for the
+controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper
+(`app/helpers/blorgh/articles_controller.rb`).
Everything this generator has created is neatly namespaced. The controller's
class is defined within the `Blorgh` module:
```ruby
module Blorgh
- class PostsController < ApplicationController
+ class ArticlesController < ApplicationController
...
end
end
@@ -382,22 +382,22 @@ end
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:
+The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced:
```ruby
module Blorgh
- module PostsHelper
+ module ArticlesHelper
...
end
end
```
This helps prevent conflicts with any other engine or application that may have
-a post resource as well.
+an article resource as well.
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
+`app/assets/javascripts/blorgh/articles.js` and
+`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little
later.
By default, the scaffold styling is not applied to the engine because the
@@ -412,46 +412,46 @@ tag of this layout:
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
+`http://localhost:3000/blorgh/articles` 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`.
+like a Rails application. Remember: the `Article` model is namespaced, so to
+reference it you must call it as `Blorgh::Article`.
```ruby
->> Blorgh::Post.find(1)
-=> #<Blorgh::Post id: 1 ...>
+>> Blorgh::Article.find(1)
+=> #<Blorgh::Article id: 1 ...>
```
-One final thing is that the `posts` resource for this engine should be the root
+One final thing is that the `articles` 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
+mounted, they should be shown a list of articles. 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"
+root to: "articles#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
+Now people will only need to go to the root of the engine to see all the articles,
+rather than visiting `/articles`. This means that instead of
+`http://localhost:3000/blorgh/articles`, you only need to go to
`http://localhost:3000/blorgh` now.
### Generating a Comments Resource
-Now that the engine can create new blog posts, it only makes sense to add
+Now that the engine can create new articles, 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
+model, a comment controller and then modify the articles scaffold to display
comments and allow people to create new ones.
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
+`Comment` model, with the related table having two columns: a `article_id` integer
and `text` text column.
```bash
-$ rails generate model Comment post_id:integer text:text
+$ bin/rails generate model Comment article_id:integer text:text
```
This will output the following:
@@ -474,17 +474,17 @@ table:
$ rake db:migrate
```
-To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and
+To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and
add this line before the "Edit" link:
```html+erb
<h3>Comments</h3>
-<%= render @post.comments %>
+<%= render @article.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:
+on the `Blorgh::Article` model, which there isn't right now. To define one, open
+`app/models/blorgh/article.rb` and add this line into the model:
```ruby
has_many :comments
@@ -494,7 +494,7 @@ Turning the model into this:
```ruby
module Blorgh
- class Post < ActiveRecord::Base
+ class Article < ActiveRecord::Base
has_many :comments
end
end
@@ -505,9 +505,9 @@ NOTE: Because the `has_many` is defined inside a class that is inside the
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 an article. To
+add this, put this line underneath the call to `render @article.comments` in
+`app/views/blorgh/articles/show.html.erb`:
```erb
<%= render "blorgh/comments/form" %>
@@ -519,7 +519,7 @@ directory at `app/views/blorgh/comments` and in it a new file called
```html+erb
<h3>New comment</h3>
-<%= form_for [@post, @post.comments.build] do |f| %>
+<%= form_for [@article, @article.comments.build] do |f| %>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
@@ -529,12 +529,12 @@ directory at `app/views/blorgh/comments` and in it a new file called
```
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
+to a route of `/articles/:article_id/comments` within the engine. This route doesn't
+exist at the moment, but can be created by changing the `resources :articles` line
inside `config/routes.rb` into these lines:
```ruby
-resources :posts do
+resources :articles do
resources :comments
end
```
@@ -545,7 +545,7 @@ 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
+$ bin/rails g controller comments
```
This will generate the following things:
@@ -558,8 +558,6 @@ invoke test_unit
create test/controllers/blorgh/comments_controller_test.rb
invoke helper
create app/helpers/blorgh/comments_helper.rb
-invoke test_unit
-create test/helpers/blorgh/comments_helper_test.rb
invoke assets
invoke js
create app/assets/javascripts/blorgh/comments.js
@@ -567,17 +565,17 @@ invoke css
create app/assets/stylesheets/blorgh/comments.css
```
-The form will be making a `POST` request to `/posts/:post_id/comments`, which
+The form will be making a `POST` request to `/articles/:article_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(comment_params)
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.create(comment_params)
flash[:notice] = "Comment has been created!"
- redirect_to posts_path
+ redirect_to articles_path
end
private
@@ -590,11 +588,11 @@ 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"
+"/Users/ryan/Sites/side_projects/blorgh/app/views"
```
The engine is unable to find the partial required for rendering the comments.
@@ -612,7 +610,7 @@ line inside it:
```
The `comment_counter` local variable is given to us by the `<%= render
-@post.comments %>` call, which will define it automatically and increment the
+@article.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.
@@ -625,7 +623,7 @@ 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.
+ownership for articles and comments within the engine.
### Mounting the Engine
@@ -676,7 +674,7 @@ pre-defined path which may be customizable.
### Engine setup
-The engine contains migrations for the `blorgh_posts` and `blorgh_comments`
+The engine contains migrations for the `blorgh_articles` 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:
@@ -698,7 +696,7 @@ 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_1]_create_blorgh_articles.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh
```
@@ -709,7 +707,7 @@ 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
+articles 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.
@@ -734,17 +732,19 @@ rake db:migrate SCOPE=blorgh VERSION=0
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
+the application. In the case of the `blorgh` engine, making articles 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.
+authors for an article 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`
-that represents the users of the application. It can be generated using this
-command inside the application:
+that represents the users of the application (we'll get into making this
+configurable further on). It can be generated using this command inside the
+application:
```bash
rails g model user name:string
@@ -753,14 +753,14 @@ 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.
-Also, to keep it simple, the posts form will have a new text field called
+Also, to keep it simple, the articles 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
+already has that name. The engine will then associate the article 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
+`app/views/blorgh/articles/_form.html.erb` partial inside the engine. This can be
added above the `title` field with this code:
```html+erb
@@ -770,23 +770,23 @@ added above the `title` field with this code:
</div>
```
-Next, we need to update our `Blorgh::PostController#post_params` method to
+Next, we need to update our `Blorgh::ArticleController#article_params` method to
permit the new form parameter:
```ruby
-def post_params
- params.require(:post).permit(:title, :text, :author_name)
+def article_params
+ params.require(:article).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
+The `Blorgh::Article` model should then have some code to convert the `author_name`
+field into an actual `User` object and associate it as that article's `author`
+before the article 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
+`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the
`User` class for the time being.
```ruby
@@ -803,14 +803,14 @@ private
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
+of associating the records in the `blorgh_articles` 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.
+`author_id` column added to the `blorgh_articles` table.
To generate this new column, run this command within the engine:
```bash
-$ rails g migration add_author_id_to_blorgh_posts author_id:integer
+$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
```
NOTE: Due to the migration's name and the column specification after it, Rails
@@ -828,12 +828,12 @@ $ rake blorgh:install:migrations
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
+```
+NOTE Migration [timestamp]_create_blorgh_articles.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
+[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh
```
Run the migration using:
@@ -843,20 +843,20 @@ $ 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.
+an author - represented by a record in the `users` table - with an article,
+represented by the `blorgh_articles` 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 article's page. Add this code
+above the "Title" output inside `app/views/blorgh/articles/show.html.erb`:
```html+erb
<p>
<b>Author:</b>
- <%= @post.author %>
+ <%= @article.author %>
</p>
```
-By outputting `@post.author` using the `<%=` tag, the `to_s` method will be
+By outputting `@article.author` using the `<%=` tag, the `to_s` method will be
called on the object. By default, this will look quite ugly:
```
@@ -925,15 +925,15 @@ 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 to switch the `Blorgh::Post` model over to this new setting.
+The next step is to switch the `Blorgh::Article` model over to this new setting.
Change the `belongs_to` association inside this model
-(`app/models/blorgh/post.rb`) to this:
+(`app/models/blorgh/article.rb`) to this:
```ruby
belongs_to :author, class_name: Blorgh.author_class
```
-The `set_author` method in the `Blorgh::Post` model should also use this class:
+The `set_author` method in the `Blorgh::Article` model should also use this class:
```ruby
self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
@@ -960,7 +960,7 @@ 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 return a `Class` instead of a
-`String`, we must also modify our `belongs_to` definition in the `Blorgh::Post`
+`String`, we must also modify our `belongs_to` definition in the `Blorgh::Article`
model:
```ruby
@@ -985,14 +985,14 @@ 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
+Go ahead and try to create a new article. 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
+associated with an article when it's created. This object, of course, should have
some sort of identifier by which it can be referenced.
#### General Engine Configuration
@@ -1052,6 +1052,16 @@ 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
------------------------------
@@ -1097,12 +1107,12 @@ that isn't referenced by your main application.
#### Implementing Decorator Pattern Using Class#class_eval
-**Adding** `Post#time_since_created`:
+**Adding** `Article#time_since_created`:
```ruby
-# MyApp/app/decorators/models/blorgh/post_decorator.rb
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
-Blorgh::Post.class_eval do
+Blorgh::Article.class_eval do
def time_since_created
Time.current - created_at
end
@@ -1110,20 +1120,20 @@ end
```
```ruby
-# Blorgh/app/models/post.rb
+# Blorgh/app/models/article.rb
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments
end
```
-**Overriding** `Post#summary`:
+**Overriding** `Article#summary`:
```ruby
-# MyApp/app/decorators/models/blorgh/post_decorator.rb
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
-Blorgh::Post.class_eval do
+Blorgh::Article.class_eval do
def summary
"#{title} - #{truncate(text)}"
end
@@ -1131,9 +1141,9 @@ end
```
```ruby
-# Blorgh/app/models/post.rb
+# Blorgh/app/models/article.rb
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments
def summary
"#{title}"
@@ -1149,13 +1159,13 @@ class modifications, you might want to consider using [`ActiveSupport::Concern`]
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** `Article#time_since_created` and **Overriding** `Article#summary`:
```ruby
-# MyApp/app/models/blorgh/post.rb
+# MyApp/app/models/blorgh/article.rb
-class Blorgh::Post < ActiveRecord::Base
- include Blorgh::Concerns::Models::Post
+class Blorgh::Article < ActiveRecord::Base
+ include Blorgh::Concerns::Models::Article
def time_since_created
Time.current - created_at
@@ -1168,22 +1178,22 @@ end
```
```ruby
-# Blorgh/app/models/post.rb
+# Blorgh/app/models/article.rb
-class Post < ActiveRecord::Base
- include Blorgh::Concerns::Models::Post
+class Article < ActiveRecord::Base
+ include Blorgh::Concerns::Models::Article
end
```
```ruby
-# Blorgh/lib/concerns/models/post
+# Blorgh/lib/concerns/models/article
-module Blorgh::Concerns::Models::Post
+module Blorgh::Concerns::Models::Article
extend ActiveSupport::Concern
# 'included do' causes the included code to be evaluated in the
- # context where it is included (post.rb), rather than being
- # executed in the module's context (blorgh/concerns/models/post).
+ # context where it is included (article.rb), rather than being
+ # executed in the module's context (blorgh/concerns/models/article).
included do
attr_accessor :author_name
belongs_to :author, class_name: "User"
@@ -1214,25 +1224,25 @@ 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.
-When the application is asked to render the view for `Blorgh::PostsController`'s
+When the application is asked to render the view for `Blorgh::ArticlesController`'s
index action, it will first look for the path
-`app/views/blorgh/posts/index.html.erb` within the application. If it cannot
+`app/views/blorgh/articles/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
+`app/views/blorgh/articles/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`
+Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb`
and put this content in it:
```html+erb
-<h1>Posts</h1>
-<%= link_to "New Post", new_post_path %>
-<% @posts.each do |post| %>
- <h2><%= post.title %></h2>
- <small>By <%= post.author %></small>
- <%= simple_format(post.text) %>
+<h1>Articles</h1>
+<%= link_to "New Article", new_article_path %>
+<% @articles.each do |article| %>
+ <h2><%= article.title %></h2>
+ <small>By <%= article.author %></small>
+ <%= simple_format(article.text) %>
<hr>
<% end %>
```
@@ -1249,30 +1259,30 @@ Routes inside an engine are drawn on the `Engine` class within
```ruby
Blorgh::Engine.routes.draw do
- resources :posts
+ resources :articles
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
+proxy method. Calls to normal routing methods such as `articles_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`
+For instance, the following example would go to the application's `articles_path`
+if that template was rendered from the application, or the engine's `articles_path`
if it was rendered from the engine:
```erb
-<%= link_to "Blog posts", posts_path %>
+<%= link_to "Blog articles", articles_path %>
```
-To make this route always use the engine's `posts_path` routing helper method,
+To make this route always use the engine's `articles_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 %>
+<%= link_to "Blog articles", blorgh.articles_path %>
```
If you wish to reference the application inside the engine in a similar way, use
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 455dc7bebe..048eb9a6e3 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -1,23 +1,22 @@
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.
--------------------------------------------------------------------------------
NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](http://api.rubyonrails.org/) for a complete reference.
-
Dealing with Basic Forms
------------------------
@@ -32,18 +31,14 @@ The most basic form helper is `form_tag`.
When called without arguments like this, it creates a `<form>` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability):
```html
-<form accept-charset="UTF-8" action="/home/index" method="post">
- <div style="margin:0;padding:0">
- <input name="utf8" type="hidden" value="&#x2713;" />
- <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
- </div>
+<form accept-charset="UTF-8" action="/" method="post">
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
Form contents
</form>
```
-Now, you'll notice that the HTML contains something extra: a `div` element with two hidden input elements inside. This div is important, because the form cannot be successfully submitted without it. The first input element with name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](./security.html#cross-site-request-forgery-csrf).
-
-NOTE: Throughout this guide, the `div` with the hidden input elements will be excluded from code samples for brevity.
+You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf).
### A Generic Search Form
@@ -67,14 +62,15 @@ 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"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
+<form accept-charset="UTF-8" action="/search" method="get">
+ <input name="utf8" type="hidden" value="&#x2713;" />
<label for="q">Search for:</label>
<input id="q" name="q" type="text" />
<input name="commit" type="submit" value="Search" />
</form>
```
-TIP: For every form input, an ID attribute is generated from its name ("q" in the example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
+TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML.
@@ -146,7 +142,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,
@@ -217,7 +213,7 @@ Dealing with Model Objects
### Model Object Helpers
-A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the _tag suffix, for example `text_field`, `text_area`.
+A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the `_tag` suffix, for example `text_field`, `text_area`.
For these helpers the first argument is the name of an instance variable and the second is the name of a method (usually an attribute) to call on that object. Rails will set the value of the input control to the return value of that method for the object and set an appropriate input name. If your controller has defined `@person` and that person's name is Henry then a form containing:
@@ -239,7 +235,7 @@ Rails provides helpers for displaying the validation errors associated with a mo
### Binding a Form to an Object
-While this is an increase in comfort it is far from perfect. If Person has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does.
+While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does.
Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`:
@@ -264,7 +260,7 @@ There are a few things to note here:
* `@article` is the actual object being edited.
* There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id.
* The `form_for` method yields a **form builder** object (the `f` variable).
-* Methods to create form controls are called **on** the form builder object `f`
+* Methods to create form controls are called **on** the form builder object `f`.
The resulting HTML is:
@@ -280,7 +276,7 @@ The name passed to `form_for` controls the key used in `params` to access the fo
The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder.
-You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a Person model with an associated ContactDetail model you could create a form for creating both like so:
+You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a `Person` model with an associated `ContactDetail` model you could create a form for creating both like so:
```erb
<%= form_for @person, url: {action: "create"} do |person_form| %>
@@ -350,7 +346,6 @@ form_for [:admin, :management, @article]
For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html).
-
### How do forms with PATCH, PUT, or DELETE methods work?
The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms.
@@ -365,12 +360,11 @@ output:
```html
<form accept-charset="UTF-8" action="/search" method="post">
- <div style="margin:0;padding:0">
- <input name="_method" type="hidden" value="patch" />
- <input name="utf8" type="hidden" value="&#x2713;" />
- <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
- </div>
+ <input name="_method" type="hidden" value="patch" />
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
...
+</form>
```
When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example).
@@ -435,14 +429,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:
@@ -474,11 +473,21 @@ As with other helpers, if you were to use the `select` helper on a form builder
<%= f.select(:city_id, ...) %>
```
-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.
+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
-Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a City model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them:
+Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a `City` model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them:
```erb
<% cities_array = City.all.map { |city| [city.name, city.id] } %>
@@ -525,7 +534,7 @@ Both of these families of helpers will create a series of select boxes for the d
### Barebones Helpers
-The `select_*` family of helpers take as their first argument an instance of Date, Time or DateTime that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example
+The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example
```erb
<%= select_date Date.today, prefix: :start_date %>
@@ -539,7 +548,7 @@ outputs (with actual option values omitted for brevity)
<select id="start_date_day" name="start_date[day]"> ... </select>
```
-The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual Time or Date object you would have to extract these values and pass them to the appropriate constructor, for example
+The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example
```ruby
Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
@@ -584,7 +593,7 @@ NOTE: In many cases the built-in date pickers are clumsy as they do not aid the
Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example "year" for `select_year`, "month" for `select_month` etc.) although this can be overridden with the `:field_name` option. The `:prefix` option works in the same way that it does for `select_date` and `select_time` and has the same default value.
-The first parameter specifies which value should be selected and can either be an instance of a Date, Time or DateTime, in which case the relevant component will be extracted, or a numerical value. For example
+The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example
```erb
<%= select_year(2009) %>
@@ -614,7 +623,7 @@ Rails provides the usual pair of helpers: the barebones `file_field_tag` and the
### What Gets Uploaded
-The object in the `params` hash is an instance of a subclass of IO. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of File backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example).
+The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example).
```ruby
def upload
@@ -625,7 +634,7 @@ def upload
end
```
-Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](http://www.thoughtbot.com/projects/paperclip).
+Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](https://github.com/thoughtbot/paperclip).
NOTE: If the user has not selected a file the corresponding parameter will be an empty string.
@@ -636,7 +645,7 @@ Unlike other forms making an asynchronous file upload form is not as simple as p
Customizing Form Builders
-------------------------
-As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of FormBuilder (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass FormBuilder and add the helpers there. For example
+As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example
```erb
<%= form_for @person do |f| %>
@@ -652,7 +661,7 @@ can be replaced with
<% end %>
```
-by defining a LabellingFormBuilder class similar to the following:
+by defining a `LabellingFormBuilder` class similar to the following:
```ruby
class LabellingFormBuilder < ActionView::Helpers::FormBuilder
@@ -670,7 +679,7 @@ The form builder used also determines what happens when you do
<%= render partial: f %>
```
-If `f` is an instance of FormBuilder then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class LabellingFormBuilder then the `labelling_form` partial would be rendered instead.
+If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead.
Understanding Parameter Naming Conventions
------------------------------------------
@@ -809,21 +818,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 %>
```
@@ -847,7 +856,7 @@ Or if you don't want to render an `authenticity_token` field:
Building Complex Forms
----------------------
-Many apps grow beyond simple forms editing a single object. For example when creating a Person you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary.
+Many apps grow beyond simple forms editing a single object. For example when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary.
### Configuring the Model
@@ -998,4 +1007,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 address' 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 4a5377c206..2349908979 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -8,6 +8,7 @@ After reading this guide, you will know:
* How to see which generators are available in your application.
* How to create a generator using templates.
* How Rails searches for generators before invoking them.
+* How Rails internally generates Rails code from the templates.
* How to customize your scaffold by creating new generators.
* How to customize your scaffold by changing generator templates.
* How to use fallbacks to avoid overwriting a huge set of generators.
@@ -23,19 +24,19 @@ When you create an application using the `rails` command, you are in fact using
```bash
$ rails new myapp
$ cd myapp
-$ rails generate
+$ bin/rails generate
```
You will get a list of all generators that comes with Rails. If you need a detailed description of the helper generator, for example, you can simply do:
```bash
-$ rails generate helper --help
+$ bin/rails generate helper --help
```
Creating Your First Generator
-----------------------------
-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`.
+Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options for 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:
@@ -54,13 +55,13 @@ Our new generator is quite simple: it inherits from `Rails::Generators::Base` an
To invoke our new generator, we just need to do:
```bash
-$ rails generate initializer
+$ bin/rails generate initializer
```
Before we go on, let's see our brand new generator description:
```bash
-$ rails generate initializer --help
+$ bin/rails generate initializer --help
```
Rails is usually able to generate good descriptions if a generator is namespaced, as `ActiveRecord::Generators::ModelGenerator`, but not in this particular case. We can solve this problem in two ways. The first one is calling `desc` inside our generator:
@@ -82,7 +83,7 @@ Creating Generators with Generators
Generators themselves have a generator:
```bash
-$ rails generate generator initializer
+$ bin/rails generate generator initializer
create lib/generators/initializer
create lib/generators/initializer/initializer_generator.rb
create lib/generators/initializer/USAGE
@@ -102,7 +103,7 @@ First, notice that we are inheriting from `Rails::Generators::NamedBase` instead
We can see that by invoking the description of this new generator (don't forget to delete the old generator file):
```bash
-$ rails generate initializer --help
+$ bin/rails generate initializer --help
Usage:
rails generate initializer NAME [options]
```
@@ -130,7 +131,7 @@ end
And let's execute our generator:
```bash
-$ rails generate initializer core_extensions
+$ bin/rails generate initializer core_extensions
```
We can see that now an initializer named core_extensions was created at `config/initializers/core_extensions.rb` with the contents of our template. That means that `copy_file` copied a file in our source root to the destination path we gave. The method `file_name` is automatically created when we inherit from `Rails::Generators::NamedBase`.
@@ -169,7 +170,7 @@ end
Before we customize our workflow, let's first see what our scaffold looks like:
```bash
-$ rails generate scaffold User name:string
+$ bin/rails generate scaffold User name:string
invoke active_record
create db/migrate/20130924151154_create_users.rb
create app/models/user.rb
@@ -191,8 +192,6 @@ $ rails generate scaffold User name:string
create test/controllers/users_controller_test.rb
invoke helper
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
@@ -207,7 +206,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, javascripts 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 stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following:
```ruby
config.generators do |g|
@@ -219,12 +218,12 @@ config.generators do |g|
end
```
-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.
+If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files 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
+$ bin/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
@@ -248,10 +247,10 @@ end
end
```
-We can try out our new generator by creating a helper for users:
+We can try out our new generator by creating a helper for products:
```bash
-$ rails generate my_helper products
+$ bin/rails generate my_helper products
create app/helpers/products_helper.rb
```
@@ -279,10 +278,10 @@ end
and see it in action when invoking the generator:
```bash
-$ rails generate scaffold Post body:text
+$ bin/rails generate scaffold Article body:text
[...]
invoke my_helper
- create app/helpers/posts_helper.rb
+ create app/helpers/articles_helper.rb
```
We can notice on the output that our new helper was invoked instead of the Rails default. However one thing is missing, which is tests for our new generator and to do that, we are going to reuse old helpers test generators.
@@ -342,6 +341,20 @@ end
If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`.
+Many scaffold templates in Rails are written in ERB tags which needs to be escaped, so that the output is a valid ERB code, that can be used correctly in Rails app.
+
+The following code in one of the generator file,
+
+```ruby
+<%%= stylesheet_include_tag :application %>
+```
+
+when passed through the generator, would generate the following output.
+
+```ruby
+<%= stylesheet_include_tag :application %>
+```
+
Adding Generators Fallbacks
---------------------------
@@ -365,7 +378,7 @@ end
Now, if you create a Comment scaffold, you will see that the shoulda generators are being invoked, and at the end, they are just falling back to TestUnit generators:
```bash
-$ rails generate scaffold Comment body:text
+$ bin/rails generate scaffold Comment body:text
invoke active_record
create db/migrate/20130924143118_create_comments.rb
create app/models/comment.rb
@@ -387,8 +400,6 @@ $ rails generate scaffold Comment body:text
create test/controllers/comments_controller_test.rb
invoke my_helper
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
@@ -507,7 +518,7 @@ Replaces text inside a file.
gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'
```
-Regular Expressions can be used to make this method more precise. You can also use append_file and prepend_file in the same way to place code at the beginning and end of a file respectively.
+Regular Expressions can be used to make this method more precise. You can also use `append_file` and `prepend_file` in the same way to place code at the beginning and end of a file respectively.
### `application`
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 5264f82b4b..1f91352c82 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -57,9 +57,9 @@ learned elsewhere, you may have a less happy experience.
The Rails philosophy includes two major guiding principles:
-* **Don't Repeat Yourself:** DRY is a principle of software development which
+* **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
+ 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
@@ -70,13 +70,11 @@ Creating a New Rails Project
The best way to use this guide is to follow each step as it happens, no code or
step needed to make this example application has been left out, so you can
-literally follow along step by step. You can get the complete code
-[here](https://github.com/rails/docrails/tree/master/guides/code/getting_started).
+literally follow along step by step.
By following along with this guide, you'll create a Rails project called
-`blog`, a
-(very) simple weblog. Before you can start building the application, you need to
-make sure that you have Rails itself installed.
+`blog`, a (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 `$` to represent your terminal prompt in a UNIX-like OS,
though it may have been customized to appear differently. If you are using Windows,
@@ -89,9 +87,9 @@ 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
+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).
+while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp).
```bash
$ ruby -v
@@ -99,7 +97,7 @@ ruby 2.0.0p353
```
If you don't have Ruby installed have a look at
-[ruby-lang.org](https://www.ruby-lang.org/en/downloads/) for possible ways to
+[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
@@ -125,7 +123,7 @@ run the following:
$ rails --version
```
-If it says something like "Rails 4.1.0", you are ready to continue.
+If it says something like "Rails 4.2.0", you are ready to continue.
### Creating the Blog Application
@@ -163,11 +161,11 @@ 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.|
+|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, deploy or run your application.|
|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://bundler.io).|
|lib/|Extended modules for your application.|
|log/|Application log files.|
|public/|The only folder seen by the world as-is. Contains static files and compiled assets.|
@@ -190,23 +188,24 @@ start a web server on your development machine. You can do this by running the
following in the `blog` directory:
```bash
-$ rails server
+$ bin/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 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).
+TIP: Compiling CoffeeScript and JavaScript asset compression requires you
+have a JavaScript runtime available on your system, in the absence
+of a runtime you will see an `execjs` error during asset compilation.
+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 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.jpg)
+![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
@@ -243,7 +242,7 @@ tell it you want a controller called "welcome" with an action called "index",
just like this:
```bash
-$ rails generate controller welcome index
+$ bin/rails generate controller welcome index
```
Rails will create several files and a route for you.
@@ -258,8 +257,6 @@ invoke test_unit
create test/controllers/welcome_controller_test.rb
invoke helper
create app/helpers/welcome_helper.rb
-invoke test_unit
-create test/helpers/welcome_helper_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/welcome.js.coffee
@@ -267,8 +264,9 @@ 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
@@ -344,7 +342,7 @@ 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 :articles
@@ -358,7 +356,7 @@ 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
+$ bin/rake routes
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
@@ -396,7 +394,7 @@ a controller called `ArticlesController`. You can do this by running this
command:
```bash
-$ rails g controller articles
+$ bin/rails g controller articles
```
If you open up the newly generated `app/controllers/articles_controller.rb`
@@ -427,19 +425,22 @@ 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/articles_controller.rb` and inside the `ArticlesController`
-class, define a `new` method like this:
+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 `ArticlesController`, if you refresh
<http://localhost:3000/articles/new> you'll see another error:
-![Template is missing for articles/new](images/getting_started/template_is_missing_articles_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
@@ -448,9 +449,7 @@ available, Rails errors out.
In the above image, the bottom line has been truncated. Let's see what the full
thing looks like:
-<blockquote>
-Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
-</blockquote>
+>Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
That's quite a lot of text! Let's quickly go through and understand what each
part of it does.
@@ -496,8 +495,8 @@ 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
+To create a form within this template, you will use a *form
+builder*. 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/articles/new.html.erb`:
@@ -552,7 +551,7 @@ To see what Rails will do with this, we look back at the output of
`rake routes`:
```bash
-$ rake routes
+$ bin/rake routes
Prefix Verb URI Pattern Controller#Action
articles GET /articles(.:format) articles#index
POST /articles(.:format) articles#create
@@ -565,18 +564,18 @@ edit_article GET /articles/:id/edit(.:format) articles#edit
root GET / welcome#index
```
-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`.
+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
article, so go ahead and do that. When you submit the form, you should see a
familiar error:
-![Unknown action create for ArticlesController](images/getting_started/unknown_action_create_for_articles.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 `ArticlesController` for
this to work.
@@ -585,7 +584,7 @@ this to work.
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:
+underneath the `new` action, as shown:
```ruby
class ArticlesController < ApplicationController
@@ -608,17 +607,19 @@ look like, change the `create` action to this:
```ruby
def create
- render text: params[:article].inspect
+ render plain: params[:article].inspect
end
```
-The `render` method here is taking a very simple hash with a key of `text` and
+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.
+TIP: Ensure you have a firm grasp of the `params` method, as you'll use it fairly regularly. Let's consider an example URL: **http://www.example.com/?username=dhh&email=dhh@email.com**. In this URL, `params[:username]` would equal "dhh" and `params[:email]` would equal "dhh@email.com".
+
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:
@@ -632,13 +633,13 @@ parameters but nothing in particular is being done with them.
### 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 Article title:string text:text
+$ bin/rails generate model Article title:string text:text
```
With that command we told Rails that we want a `Article` model, together
@@ -646,26 +647,23 @@ with a _title_ attribute of type string, and a _text_ attribute
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/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.
+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/20140120191729_create_articles.rb` file (remember,
yours will have a slightly different name), here's what you'll find:
@@ -690,13 +688,13 @@ 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:
```bash
-$ rake db:migrate
+$ bin/rake db:migrate
```
Rails will execute this migration command and tell you it created the Articles
@@ -733,26 +731,48 @@ 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 `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.
+database columns. In the first line we do just that (remember that
+`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: As we'll see later, `@article.save` returns a boolean indicating
-whether the article was saved or not.
+TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `\models\article.rb`. Class names in Ruby must begin with a capital letter.
-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:
+TIP: As we'll see later, `@article.save` returns a boolean indicating whether
+the article was saved or not.
-![Forbidden attributes for new article](images/getting_started/forbidden_attributes_for_new_article.png)
+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:
+
+![Forbidden attributes for new article]
+(images/getting_started/forbidden_attributes_for_new_article.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:
+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
+ @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
@@ -768,20 +788,15 @@ private
end
```
-See the `permit`? It allows us to accept both `title` and `text` in this
-action.
-
-TIP: Note that `def article_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 article about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
+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.
+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:
@@ -796,10 +811,24 @@ 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/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
- @article = Article.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 `Article.find` to find the article we're
@@ -838,15 +867,27 @@ articles GET /articles(.:format) articles#index
```
Add the corresponding `index` action for that route inside the
-`ArticlesController` in the `app/controllers/articles_controller.rb` file:
+`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
-def index
- @articles = Article.all
-end
+class ArticlesController < ApplicationController
+ def index
+ @articles = Article.all
+ end
+
+ def show
+ @article = Article.find(params[:id])
+ end
+
+ def new
+ end
+
+ # snipped for brevity
```
-And then finally, add view for this action, located at
+And then finally, add the view for this action, located at
`app/views/articles/index.html.erb`:
```html+erb
@@ -867,7 +908,7 @@ And then finally, add view for this action, located at
</table>
```
-Now if you go to `http://localhost:3000/articles` you will see a list of all the
+Now if you go to <http://localhost:3000/articles> you will see a list of all the
articles that you have created.
### Adding links
@@ -896,18 +937,18 @@ Let's add links to the other views as well, starting with adding this
This link will allow you to bring up the form that lets you create a new article.
-Also add a link in `app/views/articles/new.html.erb`, underneath the form, to
-go back to the `index` action:
+Now, add another link in `app/views/articles/new.html.erb`, underneath the
+form, to go back to the `index` action:
```erb
-<%= form_for :article do |f| %>
+<%= form_for :article, url: articles_path do |f| %>
...
<% end %>
<%= link_to 'Back', articles_path %>
```
-Finally, add another link to the `app/views/articles/show.html.erb` template to
+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:
@@ -925,9 +966,9 @@ article can go back and view the whole list again:
<%= 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
@@ -962,7 +1003,7 @@ 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)
+Record Validations](active_record_validations.html).
With the validation now in place, when you call `@article.save` on an invalid
article, it will return `false`. If you open
@@ -1011,17 +1052,21 @@ something went wrong. To do that, you'll modify
```html+erb
<%= 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>
+ <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 %>
@@ -1035,6 +1080,7 @@ something went wrong. To do that, you'll modify
<p>
<%= f.submit %>
</p>
+
<% end %>
<%= link_to 'Back', articles_path %>
@@ -1058,7 +1104,7 @@ standout.
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).
+<http://localhost:3000/articles/new>:
![Form With Errors](images/getting_started/form_with_errors.png)
@@ -1067,12 +1113,27 @@ you attempt to do just that on the new article form
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 the `ArticlesController`.
+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
@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
@@ -1083,17 +1144,21 @@ it look as follows:
<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>
+ <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 %>
@@ -1107,6 +1172,7 @@ it look as follows:
<p>
<%= f.submit %>
</p>
+
<% end %>
<%= link_to 'Back', articles_path %>
@@ -1119,12 +1185,28 @@ 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/articles_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
@article = Article.find(params[:id])
@@ -1166,14 +1248,14 @@ it appear next to the "Show" link:
<th colspan="2"></th>
</tr>
-<% @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 %>
+ <% @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>
```
@@ -1184,8 +1266,8 @@ bottom of the template:
```html+erb
...
-<%= link_to 'Back', articles_path %>
-| <%= link_to 'Edit', edit_article_path(@article) %>
+<%= link_to 'Back', articles_path %> |
+<%= link_to 'Edit', edit_article_path(@article) %>
```
And here's how our app looks so far:
@@ -1194,10 +1276,10 @@ 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.
@@ -1207,17 +1289,21 @@ content:
```html+erb
<%= 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>
+ <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 %>
@@ -1231,6 +1317,7 @@ content:
<p>
<%= f.submit %>
</p>
+
<% end %>
```
@@ -1239,8 +1326,8 @@ 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](//api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style).
+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:
@@ -1281,9 +1368,11 @@ people to craft malicious URLs like this:
<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/articles_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
@@ -1294,13 +1383,67 @@ def destroy
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)
+
+ 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
+```
+
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/articles/index.html.erb`) to wrap everything
-together.
+(`app/views/articles/index.html.erb`) to wrap everything together.
```html+erb
<h1>Listing Articles</h1>
@@ -1312,16 +1455,17 @@ together.
<th colspan="3"></th>
</tr>
-<% @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 %>
+ <% @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>
```
@@ -1339,9 +1483,8 @@ Without this file, the confirmation dialog box wouldn't appear.
Congratulations, you can now create, show, list, update and destroy
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
@@ -1357,7 +1500,7 @@ 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 article:references
+$ bin/rails generate model Comment commenter:string body:text article:references
```
This command will generate four files:
@@ -1390,6 +1533,8 @@ class CreateComments < ActiveRecord::Migration
create_table :comments do |t|
t.string :commenter
t.text :body
+
+ # this line adds an integer column called `article_id`.
t.references :article, index: true
t.timestamps
@@ -1403,7 +1548,7 @@ the two models. An index for this association is also created on this column.
Go ahead and run the migration:
```bash
-$ rake db:migrate
+$ bin/rake db:migrate
```
Rails is smart enough to only execute the migrations that have not already been
@@ -1479,7 +1624,7 @@ With the model in hand, you can turn your attention to creating a matching
controller. Again, we'll use the same generator we used before:
```bash
-$ rails generate controller Comments
+$ bin/rails generate controller Comments
```
This creates six files and one empty directory:
@@ -1490,7 +1635,6 @@ This creates six files and one empty directory:
| app/views/comments/ | Views of the controller are stored here |
| test/controllers/comments_controller_test.rb | The test for the controller |
| app/helpers/comments_helper.rb | A view helper file |
-| test/helpers/comments_helper_test.rb | The test for the helper |
| app/assets/javascripts/comment.js.coffee | CoffeeScript for the controller |
| app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller |
@@ -1529,8 +1673,8 @@ So first, we'll wire up the Article show template
</p>
<% end %>
-<%= link_to 'Back', articles_path %>
-| <%= link_to 'Edit', edit_article_path(@article) %>
+<%= link_to 'Back', articles_path %> |
+<%= link_to 'Edit', edit_article_path(@article) %>
```
This adds a form on the `Article` show page that creates a new comment by
@@ -1724,7 +1868,7 @@ Then you make the `app/views/articles/show.html.erb` look like the following:
<%= render @article.comments %>
<h2>Add a comment:</h2>
-<%= render "comments/form" %>
+<%= render 'comments/form' %>
<%= link_to 'Edit Article', edit_article_path(@article) %> |
<%= link_to 'Back to Articles', articles_path %>
@@ -1800,8 +1944,8 @@ database and send us back to the show action for the article.
### Deleting Associated Objects
-If you delete an article then its associated comments will also need to be
-deleted. Otherwise they would simply occupy space in the database. Rails allows
+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:
@@ -1818,21 +1962,21 @@ Security
### Basic Authentication
-If you were to publish your blog online, anybody would be able to add, edit and
+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 `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, 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
-`ArticlesController`, 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/articles_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 ArticlesController < ApplicationController
@@ -1856,14 +2000,14 @@ class CommentsController < ApplicationController
def create
@article = Article.find(params[:article_id])
- ...
+ # ...
end
# snipped for brevity
```
Now if you try to create a new article, you will be greeted with a basic HTTP
-Authentication challenge
+Authentication challenge:
![Basic HTTP Authentication Challenge](images/getting_started/challenge.png)
@@ -1878,7 +2022,7 @@ along with a number of others.
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)
+the [Ruby on Rails Security Guide](security.html).
What's Next?
@@ -1889,7 +2033,7 @@ update it and experiment on your own. But you don't have to do everything
without help. As you need assistance getting up and running with Rails, feel
free to consult these support resources:
-* The [Ruby on Rails guides](index.html)
+* The [Ruby on Rails Guides](index.html)
* The [Ruby on Rails Tutorial](http://railstutorial.org/book)
* 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
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index d72717fa3b..1023598aa4 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -92,7 +92,7 @@ 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"
```
@@ -107,7 +107,7 @@ The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths
NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced.
-The default `application.rb` files has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines.
+The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines.
```ruby
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
@@ -137,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 *cookie*. 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 [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). 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:
@@ -179,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
```
@@ -192,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
```
@@ -212,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
```
@@ -263,7 +262,7 @@ 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.)
-NOTE: Have a look at two plugins which simplify work with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master).
+NOTE: Have a look at two plugins which simplify working with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master).
### Setting the Locale from the Client Supplied Information
@@ -310,7 +309,7 @@ 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
```
@@ -370,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!
@@ -422,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:
@@ -438,11 +437,11 @@ TIP: Right now you might need to add some more date/time formats in order to mak
### Inflection Rules For Other Locales
-Rails 4.0 allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit.
+Rails allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit.
### Localized Views
-Rails 2.3 introduces another convenient localization feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.)
+Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.)
You can make use of this feature, e.g. when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them.
@@ -681,62 +680,13 @@ NOTE: Automatic conversion to HTML safe translate text is only available from th
![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:
-
-```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.
-
### Translations for Active Record Models
You can use the methods `Model.model_name.human` and `Model.human_attribute_name(attribute)` to transparently look up translations for your model and attribute names.
For example when you add the following translations:
-```ruby
+```yaml
en:
activerecord:
models:
@@ -751,7 +701,7 @@ Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("
You can also set a plural form for model names, adding as following:
-```ruby
+```yaml
en:
activerecord:
models:
@@ -762,6 +712,19 @@ en:
Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude".
+In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file:
+
+```yaml
+en:
+ activerecord:
+ attributes:
+ user/gender:
+ female: "Female"
+ male: "Male"
+```
+
+Then `User.human_attribute_name("gender.female")` will return "Female".
+
#### 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.
@@ -897,6 +860,24 @@ en:
subject: "Welcome to Rails Guides!"
```
+To send parameters to interpolation use the `default_i18n_subject` method on the mailer.
+
+```ruby
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+ def welcome(user)
+ mail(to: user.email, subject: default_i18n_subject(user: user.name))
+ end
+end
+```
+
+```yaml
+en:
+ user_mailer:
+ welcome:
+ subject: "%{user}, welcome to Rails Guides!"
+```
+
### Overview of Other Built-In Methods that Provide I18n Support
Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here's a brief overview.
@@ -921,6 +902,55 @@ Rails uses fixed strings and other localizations, such as format strings and oth
* `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
-------------------------
diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb
index 57c224c165..2fdf18a2e9 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -9,6 +9,7 @@ Ruby on Rails Guides
<% content_for :index_section do %>
<div id="subCol">
<dl>
+ <dt></dt>
<dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd>
<dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd>
</dl>
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index ec3cec5c6f..b81b048c35 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -98,9 +98,9 @@ configure the load path for your Gemfile's dependencies.
A standard Rails application depends on several gems, specifically:
-* abstract
* actionmailer
* actionpack
+* actionview
* activemodel
* activerecord
* activesupport
@@ -119,7 +119,7 @@ A standard Rails application depends on several gems, specifically:
* rails
* railties
* rake
-* sqlite3-ruby
+* sqlite3
* thor
* treetop
* tzinfo
@@ -166,6 +166,7 @@ is called.
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
@@ -178,8 +179,7 @@ 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"))
+ Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end
def server
@@ -187,6 +187,8 @@ def server
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
@@ -207,6 +209,7 @@ sets up the `Rails::Server` class.
require 'fileutils'
require 'optparse'
require 'action_dispatch'
+require 'rails'
module Rails
class Server < ::Rack::Server
@@ -273,7 +276,7 @@ def parse_options(args)
# 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
@@ -284,18 +287,21 @@ 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",
- pid: nil,
- Port: 9292,
- Host: "0.0.0.0",
- AccessLog: [],
- config: "config.ru"
+ :environment => environment,
+ :pid => nil,
+ :Port => 9292,
+ :Host => default_host,
+ :AccessLog => [],
+ :config => "config.ru"
}
end
```
-There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`
+There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`:
```ruby
def opt_parser
@@ -348,6 +354,7 @@ private
def print_boot_information
...
puts "=> Run `rails server -h` for more startup options"
+ ...
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
end
@@ -434,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
@@ -443,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:
@@ -459,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:
@@ -473,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!
@@ -498,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"
@@ -526,8 +558,8 @@ The rest of `config/application.rb` defines the configuration for the
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
-defined in `rails/application.rb`
+`Blog`, here we would find `Rails.application.initialize!`, which is
+defined in `rails/application.rb`.
### `railties/lib/rails/application.rb`
@@ -543,7 +575,7 @@ end
```
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`
+the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`:
```ruby
def run_initializers(group=:default, *args)
@@ -555,7 +587,7 @@ def run_initializers(group=:default, *args)
end
```
-The run_initializers code itself is tricky. What Rails is doing here is
+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
@@ -568,7 +600,7 @@ 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`
+After this is done we go back to `Rack::Server`.
### Rack: lib/rack/server.rb
@@ -576,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
@@ -585,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
@@ -603,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
@@ -611,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
```
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index 0513066f5a..1005057ca9 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -1,10 +1,9 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!DOCTYPE html>
<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" />
@@ -36,7 +35,6 @@
<li class="more-info"><a href="https://github.com/rails/rails">Code</a></li>
<li class="more-info"><a href="http://rubyonrails.org/screencasts">Screencasts</a></li>
<li class="more-info"><a href="http://rubyonrails.org/documentation">Documentation</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/ecosystem">Ecosystem</a></li>
<li class="more-info"><a href="http://rubyonrails.org/community">Community</a></li>
<li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
</ul>
@@ -78,7 +76,6 @@
</select>
</li>
</ul>
- </div>
</div>
</div>
<hr class="hide" />
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 93e25d619e..f00f7bca1b 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -236,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 text: "OK"
+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.
+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 `: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: 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 html: "<strong>Not Found</strong>".html_safe
+```
+
+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: This option will escape HTML entities if the string is not html safe.
#### Rendering JSON
@@ -276,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 overridden, 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:
@@ -471,33 +506,33 @@ Layout declarations cascade downward in the hierarchy, and more specific layout
end
```
-* `posts_controller.rb`
+* `articles_controller.rb`
```ruby
- class PostsController < ApplicationController
+ class ArticlesController < ApplicationController
end
```
-* `special_posts_controller.rb`
+* `special_articles_controller.rb`
```ruby
- class SpecialPostsController < PostsController
+ class SpecialArticlesController < ArticlesController
layout "special"
end
```
-* `old_posts_controller.rb`
+* `old_articles_controller.rb`
```ruby
- class OldPostsController < SpecialPostsController
+ class OldArticlesController < SpecialArticlesController
layout false
def show
- @post = Post.find(params[:id])
+ @article = Article.find(params[:id])
end
def index
- @old_posts = Post.older
+ @old_articles = Article.older
render layout: "old"
end
# ...
@@ -507,10 +542,10 @@ Layout declarations cascade downward in the hierarchy, and more specific layout
In this application:
* In general, views will be rendered in the `main` layout
-* `PostsController#index` will use the `main` layout
-* `SpecialPostsController#index` will use the `special` layout
-* `OldPostsController#show` will use no layout at all
-* `OldPostsController#index` will use the `old` layout
+* `ArticlesController#index` will use the `main` layout
+* `SpecialArticlesController#index` will use the `special` layout
+* `OldArticlesController#show` will use no layout at all
+* `OldArticlesController#index` will use the `old` layout
#### Avoiding Double Render Errors
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
index 93729c6f72..6f8584b3b7 100644
--- a/guides/source/maintenance_policy.md
+++ b/guides/source/maintenance_policy.md
@@ -3,10 +3,29 @@ 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
+follows, all versions in `X.Y.Z` format.
--------------------------------------------------------------------------------
+Rails follows a shifted version of [semver](http://semver.org/):
+
+**Patch `Z`**
+
+Only bug fixes, no API changes, no new features.
+Except as necessary for security fixes.
+
+**Minor `Y`**
+
+New features, may contain API changes (Serve as major versions of Semver).
+Breaking changes are paired with deprecation notices in the previous minor
+or major release.
+
+**Major `X`**
+
+New features, will likely contain API changes. The difference between Rails'
+minor and major releases is the magnitude of breaking changes, and usually
+reserved for special occasions.
+
New Features
------------
@@ -20,7 +39,7 @@ 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.0.z
+**Currently included series:** `4.1.Z`, `4.0.Z`.
Security Issues
---------------
@@ -35,7 +54,7 @@ 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.0.z, 3.2.z
+**Currently included series:** `4.1.Z`, `4.0.Z`.
Severe Security Issues
----------------------
@@ -44,7 +63,7 @@ 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.0.z, 3.2.z
+**Currently included series:** `4.1.Z`, `4.0.Z`, `3.2.Z`.
Unsupported Release Series
--------------------------
diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md
index 855fab18e3..4f0634d955 100644
--- a/guides/source/nested_model_forms.md
+++ b/guides/source/nested_model_forms.md
@@ -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=`.
@@ -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 720ca5d117..f10699fbeb 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -39,13 +39,13 @@ to run integration tests using a dummy Rails application. Create your
plugin with the command:
```bash
-$ rails plugin new yaffle
+$ bin/rails plugin new yaffle
```
See usage and options by asking for help:
```bash
-$ rails plugin --help
+$ bin/rails plugin new --help
```
Testing Your Newly Generated Plugin
@@ -92,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.
-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
@@ -124,7 +124,7 @@ To test that your method does what it says it does, run the unit tests with `rak
To see this in action, change to the test/dummy directory, fire up a console and start squawking:
```bash
-$ rails console
+$ bin/rails console
>> "Hello World".to_squawk
=> "squawk! Hello World"
```
@@ -149,7 +149,7 @@ end
```ruby
# yaffle/lib/yaffle.rb
-require "yaffle/core_ext"
+require 'yaffle/core_ext'
require 'yaffle/acts_as_yaffle'
module Yaffle
@@ -214,8 +214,8 @@ test/dummy directory:
```bash
$ cd test/dummy
-$ rails generate model Hickwall last_squawk:string
-$ rails generate model Wickwall last_squawk:string last_tweet:string
+$ bin/rails generate model Hickwall last_squawk:string
+$ bin/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
@@ -223,7 +223,7 @@ and migrating the database. First, run:
```bash
$ cd test/dummy
-$ rake db:migrate
+$ bin/rake db:migrate
```
While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act
@@ -433,12 +433,12 @@ Once your README is solid, go through and add rdoc comments to all of the method
Once your comments are good to go, navigate to your plugin directory and run:
```bash
-$ rake rdoc
+$ bin/rake rdoc
```
### References
* [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md)
* [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/)
-* [Gemspec Reference](http://docs.rubygems.org/read/chapter/20)
+* [Gemspec Reference](http://guides.rubygems.org/specification-reference/)
* [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins)
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index e4222e1283..6512b14e60 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -23,8 +23,8 @@ $ rails new blog -m http://example.com/template.rb
You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL.
```bash
-$ rake rails:template LOCATION=~/template.rb
-$ rake rails:template LOCATION=http://example.com/template.rb
+$ bin/rake rails:template LOCATION=~/template.rb
+$ bin/rake rails:template LOCATION=http://example.com/template.rb
```
Template API
@@ -38,9 +38,11 @@ generate(:scaffold, "person name:string")
route "root to: 'people#index'"
rake("db:migrate")
-git :init
-git add: "."
-git commit: %Q{ -m 'Initial commit' }
+after_bundle do
+ git :init
+ git add: "."
+ git commit: %Q{ -m 'Initial commit' }
+end
```
The following sections outline the primary methods provided by the API:
@@ -228,6 +230,22 @@ git add: "."
git commit: "-a -m 'Initial commit'"
```
+### after_bundle(&block)
+
+Registers a callback to be executed after the gems are bundled and binstubs
+are generated. Useful for all generated files to version control:
+
+```ruby
+after_bundle do
+ git :init
+ git add: '.'
+ git commit: "-a -m 'Initial commit'"
+end
+```
+
+The callbacks gets executed even if `--skip-bundle` and/or `--skip-spring` has
+been passed.
+
Advanced Usage
--------------
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index 9c92cf3aea..01941fa338 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -18,7 +18,7 @@ Introduction to Rack
Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
-- [Rack API Documentation](http://rack.rubyforge.org/doc/)
+* [Rack API Documentation](http://rack.github.io/)
Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below.
@@ -27,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`
@@ -112,7 +111,7 @@ NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`,
Rails has a handy rake task for inspecting the middleware stack in use:
```bash
-$ rake middleware
+$ bin/rake middleware
```
For a freshly generated Rails application, this might produce something like:
@@ -141,7 +140,7 @@ use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
-run MyApp::Application.routes
+run Rails.application.routes
```
The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below.
@@ -195,13 +194,13 @@ And now if you inspect the middleware stack, you'll find that `Rack::Lock` is
not a part of it.
```bash
-$ rake middleware
+$ bin/rake middleware
(in /Users/lifo/Rails/blog)
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:
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 9c495bf09d..af8c1bbcc4 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -183,61 +183,61 @@ You may wish to organize groups of controllers under a namespace. Most commonly,
```ruby
namespace :admin do
- resources :posts, :comments
+ resources :articles, :comments
end
```
-This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create:
+This will create a number of routes for each of the `articles` and `comments` controller. For `Admin::ArticlesController`, Rails will create:
-| 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) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | ------------------------ | ---------------------- | ---------------------------- |
+| GET | /admin/articles | admin/articles#index | admin_articles_path |
+| GET | /admin/articles/new | admin/articles#new | new_admin_article_path |
+| POST | /admin/articles | admin/articles#create | admin_articles_path |
+| GET | /admin/articles/:id | admin/articles#show | admin_article_path(:id) |
+| GET | /admin/articles/:id/edit | admin/articles#edit | edit_admin_article_path(:id) |
+| PATCH/PUT | /admin/articles/:id | admin/articles#update | admin_article_path(:id) |
+| DELETE | /admin/articles/:id | admin/articles#destroy | admin_article_path(:id) |
-If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use:
+If you want to route `/articles` (without the prefix `/admin`) to `Admin::ArticlesController`, you could use:
```ruby
scope module: 'admin' do
- resources :posts, :comments
+ resources :articles, :comments
end
```
or, for a single case:
```ruby
-resources :posts, module: 'admin'
+resources :articles, module: 'admin'
```
-If you want to route `/admin/posts` to `PostsController` (without the `Admin::` module prefix), you could use:
+If you want to route `/admin/articles` to `ArticlesController` (without the `Admin::` module prefix), you could use:
```ruby
scope '/admin' do
- resources :posts, :comments
+ resources :articles, :comments
end
```
or, for a single case:
```ruby
-resources :posts, path: '/admin/posts'
+resources :articles, path: '/admin/articles'
```
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 | 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) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | ------------------------ | -------------------- | ---------------------- |
+| GET | /admin/articles | articles#index | articles_path |
+| GET | /admin/articles/new | articles#new | new_article_path |
+| POST | /admin/articles | articles#create | articles_path |
+| GET | /admin/articles/:id | articles#show | article_path(:id) |
+| GET | /admin/articles/:id/edit | articles#edit | edit_article_path(:id) |
+| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) |
+| DELETE | /admin/articles/:id | articles#destroy | article_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'`._
@@ -304,7 +304,7 @@ TIP: _Resources should never be nested more than 1 level deep._
One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource, like this:
```ruby
-resources :posts do
+resources :articles do
resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]
@@ -313,7 +313,7 @@ resources :comments, only: [:show, :edit, :update, :destroy]
This idea strikes a balance between descriptive routes and deep nesting. There exists shorthand syntax to achieve just that, via the `:shallow` option:
```ruby
-resources :posts do
+resources :articles do
resources :comments, shallow: true
end
```
@@ -321,7 +321,7 @@ end
This will generate the exact same routes as the first example. You can also specify the `:shallow` option in the parent resource, in which case all of the nested resources will be shallow:
```ruby
-resources :posts, shallow: true do
+resources :articles, shallow: true do
resources :comments
resources :quotes
resources :drafts
@@ -332,7 +332,7 @@ The `shallow` method of the DSL creates a scope inside of which every nesting is
```ruby
shallow do
- resources :posts do
+ resources :articles do
resources :comments
resources :quotes
resources :drafts
@@ -344,7 +344,7 @@ There exist two options for `scope` to customize shallow routes. `:shallow_path`
```ruby
scope shallow_path: "sekret" do
- resources :posts do
+ resources :articles do
resources :comments, shallow: true
end
end
@@ -352,21 +352,21 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Controller#Action | Named Helper |
-| --------- | -------------------------------------- | ----------------- | ------------------- |
-| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments |
-| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments |
-| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment |
-| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment |
-| GET | /sekret/comments/:id(.:format) | comments#show | comment |
-| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment |
-| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------------------------- | ----------------- | ------------------------ |
+| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
+| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
+| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_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:
```ruby
scope shallow_prefix: "sekret" do
- resources :posts do
+ resources :articles do
resources :comments, shallow: true
end
end
@@ -374,15 +374,15 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Controller#Action | Named Helper |
-| --------- | -------------------------------------- | ----------------- | ------------------- |
-| GET | /posts/:post_id/comments(.:format) | comments#index | post_comments |
-| POST | /posts/:post_id/comments(.:format) | comments#create | post_comments |
-| GET | /posts/:post_id/comments/new(.:format) | comments#new | new_post_comment |
-| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment |
-| GET | /comments/:id(.:format) | comments#show | sekret_comment |
-| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment |
-| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------------------------- | ----------------- | --------------------------- |
+| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
+| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
+| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_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
@@ -403,7 +403,7 @@ These concerns can be used in resources to avoid code duplication and share beha
```ruby
resources :messages, concerns: :commentable
-resources :posts, concerns: [:commentable, :image_attachable]
+resources :articles, concerns: [:commentable, :image_attachable]
```
The above is equivalent to:
@@ -413,7 +413,7 @@ resources :messages do
resources :comments
end
-resources :posts do
+resources :articles do
resources :comments
resources :images, only: :index
end
@@ -422,7 +422,7 @@ end
Also you can use them in any place that you want inside the routes, for example in a scope or namespace call:
```ruby
-namespace :posts do
+namespace :articles do
concerns :commentable
end
```
@@ -645,6 +645,8 @@ match 'photos', to: 'photos#show', via: :all
NOTE: Routing both `GET` and `POST` requests to a single action has security implications. In general, you should avoid routing all verbs to an action unless you have a good reason to.
+NOTE: 'GET' in Rails won't check for CSRF token. You should never write to the database from 'GET' requests, for more information see the [security guide](security.html#csrf-countermeasures) on CSRF countermeasures.
+
### Segment Constraints
You can use the `:constraints` option to enforce a format for a dynamic segment:
@@ -662,26 +664,26 @@ get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/
`:constraints` takes regular expressions with the restriction that regexp anchors can't be used. For example, the following route will not work:
```ruby
-get '/:id', to: 'posts#show', constraints: {id: /^\d/}
+get '/:id', to: 'articles#show', constraints: { id: /^\d/ }
```
However, note that you don't need to use anchors because all routes are anchored at the start.
-For example, the following routes would allow for `posts` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace:
+For example, the following routes would allow for `articles` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace:
```ruby
-get '/:id', to: 'posts#show', constraints: { id: /\d.+/ }
+get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
get '/:username', to: 'users#show'
```
### Request-Based Constraints
-You can also constrain a route based on any method on the <a href="action_controller_overview.html#the-request-object">Request</a> object that returns a `String`.
+You can also constrain a route based on any method on the [Request object](action_controller_overview.html#the-request-object) that returns a `String`.
You specify a request-based constraint the same way that you specify a segment constraint:
```ruby
-get 'photos', constraints: {subdomain: 'admin'}
+get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }
```
You can also specify constraints in a block form:
@@ -694,6 +696,8 @@ namespace :admin do
end
```
+NOTE: Request constraints work by calling a method on the [Request object](action_controller_overview.html#the-request-object) 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:
@@ -709,7 +713,7 @@ class BlacklistConstraint
end
end
-TwitterClone::Application.routes.draw do
+Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
@@ -718,7 +722,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
@@ -769,20 +773,20 @@ get '*pages', to: 'pages#show', format: true
You can redirect any path to another path using the `redirect` helper in your router:
```ruby
-get '/stories', to: redirect('/posts')
+get '/stories', to: redirect('/articles')
```
You can also reuse dynamic segments from the match in the path to redirect to:
```ruby
-get '/stories/:name', to: redirect('/posts/%{name}')
+get '/stories/:name', to: redirect('/articles/%{name}')
```
You can also provide a block to redirect, which receives the symbolized path parameters and the request object:
```ruby
-get '/stories/:name', to: redirect {|path_params, req| "/posts/#{path_params[:name].pluralize}" }
-get '/stories', to: redirect {|path_params, req| "/posts/#{req.subdomain}" }
+get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" }
+get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }
```
Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible.
@@ -791,7 +795,7 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl
### Routing to Rack Applications
-Instead of a String like `'posts#index'`, which corresponds to the `index` action in the `PostsController`, you can specify any <a href="rails_on_rack.html">Rack application</a> as the endpoint for a matcher:
+Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher:
```ruby
match '/application.js', to: Sprockets, via: :all
@@ -799,7 +803,7 @@ match '/application.js', to: Sprockets, via: :all
As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate.
-NOTE: For the curious, `'posts#index'` actually expands out to `PostsController.action(:index)`, which returns a valid Rack application.
+NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application.
### Using `root`
@@ -835,7 +839,7 @@ get 'こんにちは', to: 'welcome#index'
Customizing Resourceful Routes
------------------------------
-While the default routes and helpers generated by `resources :posts` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers.
+While the default routes and helpers generated by `resources :articles` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers.
### Specifying a Controller to Use
@@ -877,7 +881,7 @@ a warning.
You can use the `:constraints` option to specify a required format on the implicit `id`. For example:
```ruby
-resources :photos, constraints: {id: /[A-Z][A-Z][0-9]+/}
+resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ }
```
This declaration constrains the `:id` parameter to match the supplied regular expression. So, in this case, the router would no longer match `/photos/1` to this route. Instead, `/photos/RR27` would match.
@@ -917,7 +921,7 @@ will recognize incoming paths beginning with `/photos` and route the requests to
### Overriding the `new` and `edit` Segments
-The `:path_names` option lets you override the automatically-generated "new" and "edit" segments in paths:
+The `:path_names` option lets you override the automatically-generated `new` and `edit` segments in paths:
```ruby
resources :photos, path_names: { new: 'make', edit: 'change' }
@@ -952,7 +956,7 @@ end
resources :photos
```
-This will provide route helpers such as `admin_photos_path`, `new_admin_photo_path` etc.
+This will provide route helpers such as `admin_photos_path`, `new_admin_photo_path`, etc.
To prefix a group of route helpers, use `:as` with `scope`:
@@ -972,15 +976,15 @@ You can prefix routes with a named parameter also:
```ruby
scope ':username' do
- resources :posts
+ resources :articles
end
```
-This will provide you with URLs such as `/bob/posts/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views.
+This will provide you with URLs such as `/bob/articles/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views.
### Restricting the Routes Created
-By default, Rails creates routes for the seven default actions (index, show, new, create, edit, update, and destroy) for every RESTful route in your application. You can use the `:only` and `:except` options to fine-tune this behavior. The `:only` option tells Rails to create only the specified routes:
+By default, Rails creates routes for the seven default actions (`index`, `show`, `new`, `create`, `edit`, `update`, and `destroy`) for every RESTful route in your application. You can use the `:only` and `:except` options to fine-tune this behavior. The `:only` option tells Rails to create only the specified routes:
```ruby
resources :photos, only: [:index, :show]
@@ -1042,6 +1046,28 @@ end
This will create routing helpers such as `magazine_periodical_ads_url` and `edit_magazine_periodical_ad_path`.
+### Overriding Named Route Parameters
+
+The `:param` option overrides the default resource identifier `:id` (name of
+the [dynamic segment](routing.html#dynamic-segments) used to generate the
+routes). You can access that segment from your controller using
+`params[<:param>]`.
+
+```ruby
+resources :videos, param: :identifier
+```
+
+```
+ videos GET /videos(.:format) videos#index
+ POST /videos(.:format) videos#create
+ new_videos GET /videos/new(.:format) videos#new
+edit_videos GET /videos/:identifier/edit(.:format) videos#edit
+```
+
+```ruby
+Video.find_by(identifier: params[:identifier])
+```
+
Inspecting and Testing Routes
-----------------------------
@@ -1070,7 +1096,7 @@ edit_user GET /users/:id/edit(.:format) users#edit
You may restrict the listing to the routes that map to a particular controller setting the `CONTROLLER` environment variable:
```bash
-$ CONTROLLER=users rake routes
+$ CONTROLLER=users bin/rake routes
```
TIP: You'll find that the output from `rake routes` is much more readable if you widen your terminal window until the output lines don't wrap.
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index 8faf03e58c..6206b3c715 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -13,17 +13,17 @@ After reading this guide, you will know:
Markdown
-------
-Guides are written in [GitHub Flavored Markdown](http://github.github.com/github-flavored-markdown/). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics), and [additional documentation](http://github.github.com/github-flavored-markdown/) on the differences from traditional Markdown.
+Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), as well as a [cheatsheet](http://daringfireball.net/projects/markdown/basics).
Prologue
--------
-Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. See for example the [Routing Guide](routing.html).
+Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. As an example, see the [Routing Guide](routing.html).
-Titles
+Headings
------
-The title of every guide uses `h1`; guide sections use `h2`; subsections `h3`; etc. However, the generated HTML output will have the heading tag starting from `<h2>`.
+The title of every guide uses an `h1` heading; guide sections use `h2` headings; subsections use `h3` headings; etc. Note that the generated HTML output will use heading tags starting with `<h2>`.
```
Guide Title
@@ -35,14 +35,14 @@ Section
### Sub Section
```
-Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be:
+When writing headings, capitalize all words except for prepositions, conjunctions, internal articles, and forms of the verb "to be":
```
#### Middleware Stack is an Array
#### When are Objects Saved?
```
-Use the same typography as in regular text:
+Use the same inline formatting as regular text:
```
##### The `:content_type` Option
@@ -51,25 +51,23 @@ 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. In particular, these sections of the [API Documentation Guidelines](api_documentation_guidelines.html) also apply to the guides:
* [Wording](api_documentation_guidelines.html#wording)
* [Example Code](api_documentation_guidelines.html#example-code)
-* [Filenames](api_documentation_guidelines.html#filenames)
+* [Filenames](api_documentation_guidelines.html#file-names)
* [Fonts](api_documentation_guidelines.html#fonts)
-Those guidelines apply also to guides.
-
HTML Guides
-----------
Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device.
-To install the latest version of Bundler, simply run the `gem install bundler` command
+To install the latest version of Bundler, run `gem install bundler`.
### Generation
-To generate all the guides, just `cd` into the `guides` directory, run `bundle install` and execute:
+To generate all the guides, just `cd` into the `guides` directory, run `bundle install`, and execute:
```
bundle exec rake guides:generate
diff --git a/guides/source/security.md b/guides/source/security.md
index ece431dae7..ebfcc5bdd0 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 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.
+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.
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).
@@ -25,7 +25,7 @@ The Gartner Group however estimates that 75% of attacks are at the web applicati
The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at.
-In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the <a href="#additional-resources">Additional Resources</a> chapter). I do it manually because that's how you find the nasty logical security problems.
+In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the [Additional Resources](#additional-resources) chapter). It is done manually because that's how you find the nasty logical security problems.
Sessions
--------
@@ -60,7 +60,7 @@ Many web applications have an authentication system: a user provides a user name
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
@@ -68,7 +68,7 @@ Hence, the cookie serves as temporary authentication for the web application. An
* Most people don't clear out the cookies after working at a public terminal. So if the last user didn't log out of a web application, you would be able to use it as this user. Provide the user with a _log-out button_ in the web application, and _make it prominent_.
-* 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.
+* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read [more about XSS](#cross-site-scripting-xss) 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.
@@ -95,9 +95,16 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves
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.
@@ -128,8 +135,8 @@ NOTE: _Apart from stealing a user's session id, the attacker may fix a session i
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: 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 possibly maintains the session. Expiring sessions, for example every 20 minutes, greatly reduces the time-frame for attack. Therefore they access 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.
+* 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.
@@ -144,7 +151,7 @@ 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.
@@ -180,7 +187,7 @@ This attack method works by including malicious code or a link in a page that ac
![](images/csrf.png)
-In the <a href="#sessions">session chapter</a> you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example:
+In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example:
* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file.
* `<img src="http://www.webapp.com/project/1/destroy">`
@@ -191,7 +198,7 @@ In the <a href="#sessions">session chapter</a> you have learned that most Rails
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 many security contract works - _CSRF is an important security issue_.
### CSRF Countermeasures
@@ -232,26 +239,25 @@ Or the attacker places the code into the onmouseover event handler of an image:
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:
+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
+protect_from_forgery with: :exception
```
-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, the session will be reset.
+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 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.
+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 [more about XSS](#cross-site-scripting-xss) later.
Redirection and Files
---------------------
@@ -307,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.
@@ -368,7 +374,7 @@ For _countermeasures against CSRF in administration interfaces and Intranet appl
The common admin interface works like this: it's located at www.example.com/admin, may be accessed only if the admin flag is set in the User model, re-displays user input and allows the admin to delete/add/edit whatever data desired. Here are some thoughts about this:
-* It is very important to _think about the worst case_: What if someone really got hold of my cookie or user credentials. You could _introduce roles_ for the admin interface to limit the possibilities of the attacker. Or how about _special login credentials_ for the admin interface, other than the ones used for the public part of the application. Or a _special password for very serious actions_?
+* It is very important to _think about the worst case_: What if someone really got hold of your cookies or user credentials. You could _introduce roles_ for the admin interface to limit the possibilities of the attacker. Or how about _special login credentials_ for the admin interface, other than the ones used for the public part of the application. Or a _special password for very serious actions_?
* Does the admin really have to access the interface from everywhere in the world? Think about _limiting the login to a bunch of source IP addresses_. Examine request.remote_ip to find out about the user's IP address. This is not bullet-proof, but a great barrier. Remember that there might be a proxy in use, though.
@@ -400,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 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.
+And thus it found the first user in the database, returned it and logged them in. You can find out more about it in [this 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
@@ -471,7 +477,7 @@ config.filter_parameters << :password
INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
-Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned <a href="#examples-from-the-underground">below</a>. It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
+Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey.
@@ -624,7 +630,7 @@ Also, the second query renames some columns with the AS statement so that the we
#### Countermeasures
-Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. <em class="highlight">Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure</em>. But in SQL fragments, especially <em class="highlight">in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually</em>.
+Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. *Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure*. But in SQL fragments, especially *in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually*.
Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this:
@@ -726,7 +732,7 @@ Imagine a blacklist deletes "script" from the user input. Now the attacker injec
strip_tags("some<<b>script>alert('hello')<</b>/script>")
```
-This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why I vote for a whitelist approach, using the updated Rails 2 method sanitize():
+This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why a whitelist approach is better, using the updated Rails 2 method sanitize():
```ruby
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
@@ -735,7 +741,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title))
This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags.
-As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt`;, and `&gt;`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the [SafeErb](http://safe-erb.rubyforge.org/svn/plugins/safe_erb/) plugin_. SafeErb reminds you to escape strings from external sources.
+As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt`;, and `&gt;`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources.
##### Obfuscation and Encoding Injection
@@ -806,7 +812,7 @@ The [moz-binding](http://www.securiteam.com/securitynews/5LP051FHPE.html) CSS pr
#### Countermeasures
-This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, I am not aware of a whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one.
+This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one.
### Textile Injection
@@ -996,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
@@ -1005,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 07f3aad1e6..3e12afc000 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -49,7 +49,9 @@ The `test_helper.rb` file holds the default configuration for your tests.
### The Low-Down on Fixtures
-For good tests, you'll need to give some thought to setting up test data. In Rails, you can handle this by defining and customizing fixtures.
+For good tests, you'll need to give some thought to setting up test data.
+In Rails, you can handle this by defining and customizing fixtures.
+You can find comprehensive documentation in the [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
#### What Are Fixtures?
@@ -94,6 +96,12 @@ one:
category: about
```
+Note: For associations to reference one another by name, you cannot specify the `id:`
+ attribute on the fixtures. Rails will auto assign a primary key to be consistent between
+ runs. If you manually specify an `id:` attribute, this behavior will not work. For more
+ information on this assocation behavior please read the
+ [fixture api documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
+
#### 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:
@@ -134,27 +142,27 @@ Unit Testing 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.
+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. We will be using examples from this generated code and will be supplementing it with additional examples where necessary.
-NOTE: For more information on Rails <i>scaffolding</i>, refer to [Getting Started with Rails](getting_started.html)
+NOTE: For more information on Rails _scaffolding_, refer to [Getting Started with Rails](getting_started.html)
When you use `rails generate scaffold`, for a resource among other things it creates a test stub in the `test/models` folder:
```bash
-$ rails generate scaffold post title:string body:text
+$ bin/rails generate scaffold article title:string body:text
...
-create app/models/post.rb
-create test/models/post_test.rb
-create test/fixtures/posts.yml
+create app/models/article.rb
+create test/models/article_test.rb
+create test/fixtures/articles.yml
...
```
-The default test stub in `test/models/post_test.rb` looks like this:
+The default test stub in `test/models/article_test.rb` looks like this:
```ruby
require 'test_helper'
-class PostTest < ActiveSupport::TestCase
+class ArticleTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
@@ -170,15 +178,15 @@ require 'test_helper'
As you know by now, `test_helper.rb` specifies the default configuration to run our tests. This is included with all the tests, so any methods added to this file are available to all your tests.
```ruby
-class PostTest < ActiveSupport::TestCase
+class ArticleTest < ActiveSupport::TestCase
```
-The `PostTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `PostTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide.
+The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide.
-Any method defined within a class inherited from `MiniTest::Unit::TestCase`
-(which is the superclass of `ActiveSupport::TestCase`) that begins with `test` (case sensitive) is simply called a test. So, `test_password`, `test_valid_password` and `testValidPassword` all are legal test names and are run automatically when the test case is run.
+Any method defined within a class inherited from `Minitest::Test`
+(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` (case sensitive) is simply called a test. So, `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run.
-Rails adds a `test` method that takes a test name and a block. It generates a normal `MiniTest::Unit` test with method names prefixed with `test_`. So,
+Rails adds a `test` method that takes a test name and a block. It generates a normal `Minitest::Unit` test with method names prefixed with `test_`. So,
```ruby
test "the truth" do
@@ -220,7 +228,7 @@ In order to run your tests, your test database will need to have the current str
Running a test is as simple as invoking the file containing the test cases through `rake test` command.
```bash
-$ rake test test/models/post_test.rb
+$ bin/rake test test/models/article_test.rb
.
Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
@@ -231,7 +239,7 @@ Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
You can also run a particular test method from the test case by running the test and providing the `test method name`.
```bash
-$ rake test test/models/post_test.rb test_the_truth
+$ bin/rake test test/models/article_test.rb test_the_truth
.
Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
@@ -243,25 +251,25 @@ This will run all test methods from the test case. Note that `test_helper.rb` is
The `.` (dot) above indicates a passing test. When a test fails you see an `F`; when a test throws an error you see an `E` in its place. The last line of the output is the summary.
-To see how a test failure is reported, you can add a failing test to the `post_test.rb` test case.
+To see how a test failure is reported, you can add a failing test to the `article_test.rb` test case.
```ruby
-test "should not save post without title" do
- post = Post.new
- assert !post.save
+test "should not save article without title" do
+ article = Article.new
+ assert_not article.save
end
```
Let us run this newly added test.
```bash
-$ rake test test/models/post_test.rb test_should_not_save_post_without_title
+$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title
F
Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s.
1) Failure:
-test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
+test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]:
Failed assertion, no message given.
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
@@ -270,9 +278,9 @@ Failed assertion, no message given.
In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here:
```ruby
-test "should not save post without title" do
- post = Post.new
- assert !post.save, "Saved the post without a title"
+test "should not save article without title" do
+ article = Article.new
+ assert_not article.save, "Saved the article without a title"
end
```
@@ -280,14 +288,14 @@ Running this test shows the friendlier assertion message:
```bash
1) Failure:
-test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
-Saved the post without a title
+test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]:
+Saved the article without a title
```
Now to get this test to pass we can add a model level validation for the _title_ field.
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
validates :title, presence: true
end
```
@@ -295,7 +303,7 @@ end
Now the test should pass. Let us verify by running the test again:
```bash
-$ rake test test/models/post_test.rb test_should_not_save_post_without_title
+$ bin/rake test test/models/article_test.rb test_should_not_save_article_without_title
.
Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s.
@@ -320,15 +328,15 @@ end
Now you can see even more output in the console from running the tests:
```bash
-$ rake test test/models/post_test.rb test_should_report_error
+$ bin/rake test test/models/article_test.rb test_should_report_error
E
Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s.
1) Error:
-test_should_report_error(PostTest):
-NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0>
- test/models/post_test.rb:10:in `block in <class:PostTest>'
+test_should_report_error(ArticleTest):
+NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0>
+ test/models/article_test.rb:10:in `block in <class:ArticleTest>'
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
```
@@ -345,7 +353,7 @@ backtrace. simply set the `BACKTRACE` environment variable to enable this
behavior:
```bash
-$ BACKTRACE=1 rake test test/models/post_test.rb
+$ BACKTRACE=1 bin/rake test test/models/article_test.rb
```
### What to Include in Your Unit Tests
@@ -357,7 +365,7 @@ Ideally, you would like to include a test for everything which could possibly br
By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned.
There are a bunch of different types of assertions you can use.
-Here's an extract of the assertions you can use with `minitest`, the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required.
+Here's an extract of the assertions you can use with [`Minitest`](https://github.com/seattlerb/minitest), the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required.
| Assertion | Purpose |
| ---------------------------------------------------------------- | ------- |
@@ -369,8 +377,12 @@ Here's an extract of the assertions you can use with `minitest`, the default tes
| `assert_not_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.|
| `assert_nil( obj, [msg] )` | Ensures that `obj.nil?` is true.|
| `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.|
+| `assert_empty( obj, [msg] )` | Ensures that `obj` is `empty?`.|
+| `assert_not_empty( obj, [msg] )` | Ensures that `obj` is not `empty?`.|
| `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.|
| `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.|
+| `assert_includes( collection, obj, [msg] )` | Ensures that `obj` is in `collection`.|
+| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.|
| `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are 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.|
@@ -384,16 +396,20 @@ Here's an extract of the assertions you can use with `minitest`, the default tes
| `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.|
| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.|
+| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`|
+| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`|
| `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.|
+The above are subset of assertions that minitest supports. For an exhaustive & more up-to-date list, please check [Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically [`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html)
+
Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier.
NOTE: Creating your own assertions is an advanced topic that we won't cover in this tutorial.
### Rails Specific Assertions
-Rails adds some custom assertions of its own to the `test/unit` framework:
+Rails adds some custom assertions of its own to the `minitest` framework:
| Assertion | Purpose |
| --------------------------------------------------------------------------------- | ------- |
@@ -422,26 +438,26 @@ You should test for things such as:
* was the correct object stored in the response template?
* was the appropriate message displayed to the user in the view?
-Now that we have used Rails scaffold generator for our `Post` resource, it has already created the controller code and tests. You can take look at the file `posts_controller_test.rb` in the `test/controllers` directory.
+Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory.
-Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`.
+Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`.
```ruby
-class PostsControllerTest < ActionController::TestCase
+class ArticlesControllerTest < ActionController::TestCase
test "should get index" do
get :index
assert_response :success
- assert_not_nil assigns(:posts)
+ assert_not_nil assigns(:articles)
end
end
```
-In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `posts` instance variable.
+In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `articles` instance variable.
The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments:
* The action of the controller you are requesting. This can be in the form of a string or a symbol.
-* An optional hash of request parameters to pass into the action (eg. query string parameters or post variables).
+* An optional hash of request parameters to pass into the action (eg. query string parameters or article variables).
* An optional hash of session variables to pass along with the request.
* An optional hash of flash values.
@@ -457,17 +473,17 @@ Another example: Calling the `:view` action, passing an `id` of 12 as the `param
get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})
```
-NOTE: If you try running `test_should_create_post` test from `posts_controller_test.rb` it will fail on account of the newly added model level validation and rightly so.
+NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so.
-Let us modify `test_should_create_post` test in `posts_controller_test.rb` so that all our test pass:
+Let us modify `test_should_create_article` test in `articles_controller_test.rb` so that all our test pass:
```ruby
-test "should create post" do
- assert_difference('Post.count') do
- post :create, post: {title: 'Some title'}
+test "should create article" do
+ assert_difference('Article.count') do
+ post :create, article: {title: 'Some title'}
end
- assert_redirected_to post_path(assigns(:post))
+ assert_redirected_to article_path(assigns(:article))
end
```
@@ -576,12 +592,12 @@ is the correct way to assert for the layout when the view renders a partial with
Here's another example that uses `flash`, `assert_redirected_to`, and `assert_difference`:
```ruby
-test "should create post" do
- assert_difference('Post.count') do
- post :create, post: {title: 'Hi', body: 'This is my first post.'}
+test "should create article" do
+ assert_difference('Article.count') do
+ post :create, article: {title: 'Hi', body: 'This is my first article.'}
end
- assert_redirected_to post_path(assigns(:post))
- assert_equal 'Post was successfully created.', flash[:notice]
+ assert_redirected_to article_path(assigns(:article))
+ assert_equal 'Article was successfully created.', flash[:notice]
end
```
@@ -653,7 +669,7 @@ Integration tests are used to test the interaction among any number of controlle
Unlike Unit and Functional tests, integration tests have to be explicitly created under the 'test/integration' folder within your application. Rails provides a generator to create an integration test skeleton for you.
```bash
-$ rails generate integration_test user_flows
+$ bin/rails generate integration_test user_flows
exists test/integration/
create test/integration/user_flows_test.rb
```
@@ -699,8 +715,6 @@ A simple integration test that exercises multiple controllers:
require 'test_helper'
class UserFlowsTest < ActionDispatch::IntegrationTest
- fixtures :users
-
test "login and browse site" do
# login via https
https!
@@ -712,7 +726,7 @@ class UserFlowsTest < ActionDispatch::IntegrationTest
assert_equal 'Welcome david!', flash[:notice]
https!(false)
- get "/posts/all"
+ get "/articles/all"
assert_response :success
assert assigns(:products)
end
@@ -727,10 +741,7 @@ Here's an example of multiple sessions and custom DSL in an integration test
require 'test_helper'
class UserFlowsTest < ActionDispatch::IntegrationTest
- fixtures :users
-
test "login and browse site" do
-
# User david logs in
david = login(:david)
# User guest logs in
@@ -793,57 +804,53 @@ when you initiate a Rails project.
| `rake test:all:db` | Runs all tests quickly by merging all types and resetting db |
-Brief Note About `MiniTest`
+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
-us to use all of the basic assertions in our tests.
+Ruby ships with a vast Standard Library for all common use-cases including testing. Since version 1.9, Ruby provides `Minitest`, a framework for testing. All the basic assertions such as `assert_equal` discussed above are actually defined in `Minitest::Assertions`. The classes `ActiveSupport::TestCase`, `ActionController::TestCase`, `ActionMailer::TestCase`, `ActionView::TestCase` and `ActionDispatch::IntegrationTest` - which we have been inheriting in our test classes - include `Minitest::Assertions`, 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.
-
-NOTE: For more information on `Test::Unit`, refer to [test/unit Documentation](http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/)
-For more information on `MiniTest`, refer to [Minitest](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/minitest/unit/rdoc/)
+NOTE: For more information on `Minitest`, refer to [Minitest](http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest.html)
Setup and Teardown
------------------
-If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Posts` controller:
+If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Articles` controller:
```ruby
require 'test_helper'
-class PostsControllerTest < ActionController::TestCase
+class ArticlesControllerTest < ActionController::TestCase
# called before every single test
def setup
- @post = posts(:one)
+ @article = articles(:one)
end
# called after every single test
def teardown
- # as we are re-initializing @post before every test
+ # as we are re-initializing @article before every test
# setting it to nil here is not essential but I hope
# you understand how you can use the teardown method
- @post = nil
+ @article = nil
end
- test "should show post" do
- get :show, id: @post.id
+ test "should show article" do
+ get :show, id: @article.id
assert_response :success
end
- test "should destroy post" do
- assert_difference('Post.count', -1) do
- delete :destroy, id: @post.id
+ test "should destroy article" do
+ assert_difference('Article.count', -1) do
+ delete :destroy, id: @article.id
end
- assert_redirected_to posts_path
+ assert_redirected_to articles_path
end
end
```
-Above, the `setup` method is called before each test and so `@post` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using:
+Above, the `setup` method is called before each test and so `@article` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using:
* a block
* a method (like in the earlier example)
@@ -855,38 +862,38 @@ Let's see the earlier example by specifying `setup` callback by specifying a met
```ruby
require 'test_helper'
-class PostsControllerTest < ActionController::TestCase
+class ArticlesControllerTest < ActionController::TestCase
# called before every single test
- setup :initialize_post
+ setup :initialize_article
# called after every single test
def teardown
- @post = nil
+ @article = nil
end
- test "should show post" do
- get :show, id: @post.id
+ test "should show article" do
+ get :show, id: @article.id
assert_response :success
end
- test "should update post" do
- patch :update, id: @post.id, post: {}
- assert_redirected_to post_path(assigns(:post))
+ test "should update article" do
+ patch :update, id: @article.id, article: {}
+ assert_redirected_to article_path(assigns(:article))
end
- test "should destroy post" do
- assert_difference('Post.count', -1) do
- delete :destroy, id: @post.id
+ test "should destroy article" do
+ assert_difference('Article.count', -1) do
+ delete :destroy, id: @article.id
end
- assert_redirected_to posts_path
+ assert_redirected_to articles_path
end
private
- def initialize_post
- @post = posts(:one)
+ def initialize_article
+ @article = articles(:one)
end
end
```
@@ -894,11 +901,11 @@ end
Testing Routes
--------------
-Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Posts` controller above should look like:
+Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Articles` controller above should look like:
```ruby
-test "should route to post" do
- assert_routing '/posts/1', {controller: "posts", action: "show", id: "1"}
+test "should route to article" do
+ assert_routing '/articles/1', {controller: "articles", action: "show", id: "1"}
end
```
@@ -943,7 +950,7 @@ class UserMailerTest < ActionMailer::TestCase
# 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
@@ -997,7 +1004,7 @@ class UserControllerTest < ActionController::TestCase
assert_equal "You have been invited by me@example.com", invite_email.subject
assert_equal 'friend@example.com', invite_email.to[0]
- assert_match(/Hi friend@example.com/, invite_email.body)
+ assert_match(/Hi friend@example.com/, invite_email.body.to_s)
end
end
```
@@ -1007,17 +1014,9 @@ 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
-```
+located under the `test/helpers` directory.
-The generated test file contains the following code:
+A helper test looks like so:
```ruby
require 'test_helper'
@@ -1046,11 +1045,10 @@ access to Rails' helper methods such as `link_to` or `pluralize`.
Other Testing Approaches
------------------------
-The built-in `test/unit` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including:
+The built-in `minitest` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including:
* [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.
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 8aae3bbc1a..cc20782780 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -3,6 +3,8 @@ A Guide for Upgrading Ruby on Rails
This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides.
+--------------------------------------------------------------------------------
+
General Advice
--------------
@@ -22,11 +24,76 @@ 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
+### The Rake Task
+
+Rails provides the `rails:update` rake task. After updating the Rails version
+in the Gemfile, run this rake task.
+This will help you with the creation of new files and changes of old files in a
+interactive session.
+
+```bash
+$ rake rails:update
+ identical config/boot.rb
+ exist config
+ conflict config/routes.rb
+Overwrite /myapp/config/routes.rb? (enter "h" for help) [Ynaqdh]
+ force config/routes.rb
+ conflict config/application.rb
+Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
+ force config/application.rb
+ conflict config/environment.rb
+...
+```
+
+Don't forget to review the difference, to see if there were any unexpected changes.
+
+Upgrading from Rails 4.1 to Rails 4.2
-------------------------------------
NOTE: This section is a work in progress.
+### Serialized attributes
+
+When using a custom coder (e.g. `serialize :metadata, JSON`),
+assigning `nil` to a serialized attribute will save it to the database
+as `NULL` instead of passing the `nil` value through the coder (e.g. `"null"`
+when using the `JSON` coder).
+
+### `after_bundle` in Rails templates
+
+If you have a Rails template that adds all the files in version control, it
+fails to add the generated binstubs because it gets executed before Bundler:
+
+```ruby
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+git :init
+git add: "."
+git commit: %Q{ -m 'Initial commit' }
+```
+
+You can now wrap the `git` calls in an `after_bundle` block. It will be run
+after the binstubs have been generated.
+
+```ruby
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+after_bundle do
+ git :init
+ git add: "."
+ git commit: %Q{ -m 'Initial commit' }
+end
+```
+
+Upgrading from Rails 4.0 to Rails 4.1
+-------------------------------------
+
### CSRF protection from remote `<script>` tags
Or, "whaaat my tests are failing!!!?"
@@ -79,11 +146,14 @@ secrets, you need to:
secret_key_base:
production:
- secret_key_base:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
```
-2. Copy the existing `secret_key_base` from the `secret_token.rb` initializer to
- `secrets.yml` under the `production` section.
+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.
@@ -104,13 +174,57 @@ Applications created before Rails 4.1 uses `Marshal` to serialize cookie values
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.cookies_serializer :hybrid
- ```
+```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. Looping 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.
@@ -165,6 +279,16 @@ 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.
+#### JSON representation of Time objects
+
+`#as_json` for objects with time component (`Time`, `DateTime`, `ActiveSupport::TimeWithZone`)
+now returns millisecond precision by default. If you need to keep old behavior with no millisecond
+precision, set the following in an initializer:
+
+```
+ActiveSupport::JSON::Encoding.time_precision = 0
+```
+
### Usage of `return` within inline callback blocks
Previously, Rails allowed inline callback blocks to use `return` this way:
@@ -262,6 +386,114 @@ authors = Author.where(name: 'Hank Moody').to_a
authors.compact!
```
+### Changes on Default Scopes
+
+Default scopes are no longer overridden by chained conditions.
+
+In previous versions 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.
+
+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`.
+
+### PostgreSQL json and hstore datatypes
+
+Rails 4.1 will map `json` and `hstore` columns to a string-keyed Ruby `Hash`.
+In earlier versions a `HashWithIndifferentAccess` was used. This means that
+symbol access is no longer supported. This is also the case for
+`store_accessors` based on top of `json` or `hstore` columns. Make sure to use
+string keys consistently.
+
+### Explicit block use for `ActiveSupport::Callbacks`
+
+Rails 4.1 now expects an explicit block to be passed when calling
+`ActiveSupport::Callbacks.set_callback`. This change stems from
+`ActiveSupport::Callbacks` being largely rewritten for the 4.1 release.
+
+```ruby
+# Previously in Rails 4.0
+set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+
+# Now in Rails 4.1
+set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
+```
+
Upgrading from Rails 3.2 to Rails 4.0
-------------------------------------
@@ -333,7 +565,7 @@ being used, you can update your form to use the `PUT` method instead:
<%= form_for [ :update_name, @user ], method: :put do |f| %>
```
-For more on PATCH and why this change was made, see [this post](http://weblog.rubyonrails.org/2012/2/26/edge-rails-patch-is-the-new-primary-http-method-for-updates/)
+For more on PATCH and why this change was made, see [this post](http://weblog.rubyonrails.org/2012/2/25/edge-rails-patch-is-the-new-primary-http-method-for-updates/)
on the Rails blog.
#### A note about media types
@@ -349,7 +581,7 @@ def update
respond_to do |format|
format.json do
# perform a partial update
- @post.update params[:post]
+ @article.update params[:article]
end
format.json_patch do
@@ -393,6 +625,9 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep
* 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`.
+* When using the default coder, assigning `nil` to a serialized attribute will save it
+to the database as `NULL` instead of passing the `nil` value through YAML (`"--- \n...\n"`).
+
* 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
@@ -567,9 +802,8 @@ Active Record Observer and Action Controller Sweeper have been extracted to the
### sprockets-rails
-* `assets:precompile:primary` has been removed. Use `assets:precompile` instead.
-* The `config.assets.compress` option should be changed to
-`config.assets.js_compressor` like so for instance:
+* `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
@@ -577,22 +811,23 @@ 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.
+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.16,
-the last 3.2.x version of Rails.
+The following changes are meant for upgrading your application to the latest
+3.2.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '3.2.16'
+gem 'rails', '3.2.18'
group :assets do
gem 'sass-rails', '~> 3.2.6'
@@ -753,7 +988,7 @@ AppName::Application.config.session_store :cookie_store, key: 'SOMETHINGNEW'
or
```bash
-$ rake db:sessions:clear
+$ bin/rake db:sessions:clear
```
### Remove :cache and :concat options in asset helpers references in views
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index 3c04204c29..7c3fd9f69d 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)
@@ -156,7 +158,7 @@ is a helper that assists with writing forms. `form_for` takes a `:remote`
option. It works like this:
```erb
-<%= form_for(@post, remote: true) do |f| %>
+<%= form_for(@article, remote: true) do |f| %>
...
<% end %>
```
@@ -164,7 +166,7 @@ option. It works like this:
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/posts" class="new_post" data-remote="true" id="new_post" method="post">
+<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post">
...
</form>
```
@@ -178,10 +180,10 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out:
```coffeescript
$(document).ready ->
- $("#new_post").on("ajax:success", (e, data, status, xhr) ->
- $("#new_post").append xhr.responseText
- ).bind "ajax:error", (e, xhr, status, error) ->
- $("#new_post").append "<p>ERROR</p>"
+ $("#new_article").on("ajax:success", (e, data, status, xhr) ->
+ $("#new_article").append xhr.responseText
+ ).on "ajax:error", (e, xhr, status, error) ->
+ $("#new_article").append "<p>ERROR</p>"
```
Obviously, you'll want to be a bit more sophisticated than that, but it's a
@@ -194,7 +196,7 @@ is very similar to `form_for`. It has a `:remote` option that you can use like
this:
```erb
-<%= form_tag('/posts', remote: true) do %>
+<%= form_tag('/articles', remote: true) do %>
...
<% end %>
```
@@ -202,7 +204,7 @@ this:
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/posts" data-remote="true" method="post">
+<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post">
...
</form>
```
@@ -217,21 +219,21 @@ is a helper that assists with generating links. It has a `:remote` option you
can use like this:
```erb
-<%= link_to "a post", @post, remote: true %>
+<%= link_to "an article", @article, remote: true %>
```
which generates
```html
-<a href="/posts/1" data-remote="true">a post</a>
+<a href="/articles/1" data-remote="true">an article</a>
```
You can bind to the same Ajax events as `form_for`. Here's an example. Let's
-assume that we have a list of posts that can be deleted with just one
+assume that we have a list of articles that can be deleted with just one
click. We would generate some HTML like this:
```erb
-<%= link_to "Delete post", @post, remote: true, method: :delete %>
+<%= link_to "Delete article", @article, remote: true, method: :delete %>
```
and write some CoffeeScript like this:
@@ -239,7 +241,7 @@ and write some CoffeeScript like this:
```coffeescript
$ ->
$("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
- alert "The post was deleted."
+ alert "The article was deleted."
```
### button_to
@@ -247,14 +249,14 @@ $ ->
[`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this:
```erb
-<%= button_to "A post", @post, remote: true %>
+<%= button_to "An article", @article, remote: true %>
```
this generates
```html
-<form action="/posts/1" class="button_to" data-remote="true" method="post">
- <div><input type="submit" value="A post"></div>
+<form action="/articles/1" class="button_to" data-remote="true" method="post">
+ <div><input type="submit" value="An article"></div>
</form>
```
diff --git a/guides/w3c_validator.rb b/guides/w3c_validator.rb
index 6ef3df45a9..71f044b9c4 100644
--- a/guides/w3c_validator.rb
+++ b/guides/w3c_validator.rb
@@ -60,6 +60,8 @@ module RailsGuides
def guides_to_validate
guides = Dir["./output/*.html"]
guides.delete("./output/layout.html")
+ guides.delete("./output/_license.html")
+ guides.delete("./output/_welcome.html")
ENV.key?('ONLY') ? select_only(guides) : guides
end
diff --git a/rails.gemspec b/rails.gemspec
index d1c199a97a..4800df0df4 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -27,5 +27,5 @@ Gem::Specification.new do |s|
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 bade9ef543..1ccdfb6589 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,319 +1,144 @@
-* Do not crash when `config/secrets.yml` is empty.
+* Add `after_bundle` callbacks in Rails templates. Useful for allowing the
+ generated binstubs to be added to version control.
- *Yves Senn*
-
-* Set `dump_schema_after_migration` config values in production.
-
- Set `config.active_record.dump_schema_after_migration` as false
- in the generated `config/environments/production.rb` file.
-
- *Emil Soman*
-
-* Added Thor-action for creation of migrations.
-
- Fixes #13588, #12674.
-
- *Gert Goet*
-
-* Ensure that `bin/rails` is a file before trying to execute it.
-
- Fixes #13825.
-
- *bronzle*
-
-* Use single quotes in generated files.
-
- *Cristian Mircea Messel*, *Chulki Lee*
+ Fixes #16292.
-* Only lookup `config.log_level` for stdlib `::Logger` instances.
- Assign it as is for third party loggers like `Log4r::Logger`.
+ *Stefan Kanev*
- Fixes #13421.
+* Pull in the custom configuration concept from dhh/custom_configuration, which allows you to
+ configure your own code through the Rails configuration object with custom configuration:
- *Yves Senn*
-
-* The `Gemfile` of new applications depends on SDoc ~> 0.4.0.
-
- *Xavier Noria*
-
-* `test_help.rb` now automatically checks/maintains your test database
- schema. (Use `config.active_record.maintain_test_schema = false` to
- disable.)
-
- *Jon Leighton*
-
-* Configure `secrets.yml` and `database.yml` to read configuration
- from the system environment by default for production.
-
- *José Valim*
-
-* `config.assets.raise_runtime_errors` is set to true by default
-
- This option has been introduced in
- [sprockets-rails#100][https://github.com/rails/sprockets-rails/pull/100]
- and defaults to true in new applications in development.
-
- *Richard Schneeman*
+ # config/environments/production.rb
+ config.x.payment_processing.schedule = :daily
+ config.x.payment_processing.retries = 3
+ config.x.super_debugger = true
-* Generates `html` and `text` templates for mailers by default.
+ These configuration points are then available through the configuration object:
- *Kassio Borges*
+ Rails.configuration.x.payment_processing.schedule # => :daily
+ Rails.configuration.x.payment_processing.retries # => 3
+ Rails.configuration.x.super_debugger # => true
+ Rails.configuration.x.super_debugger.not_set # => nil
-* Move `secret_key_base` from `config/initializers/secret_token.rb`
- to `config/secrets.yml`.
+ *DHH*
- `secret_key_base` is now saved in `Rails.application.secrets.secret_key_base`
- and it fallbacks to the value of `config.secret_key_base` when it is not
- present in `config/secrets.yml`.
+* Scaffold generator `_form` partial adds `class="field"` for password
+ confirmation fields.
- `config/initializers/secret_token.rb` is not generated by default
- in new applications.
+ *noinkling*
- *Guillermo Iguaran*
-
-* Generate a new `secrets.yml` file in the `config` folder for new
- applications. 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 will be accessible via `Rails.application.secrets`.
- For example, with the following `secrets.yml`:
-
- development:
- secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
- some_api_key: SOMEKEY
-
- `Rails.application.secrets.some_api_key` will return `SOMEKEY` in the development
+* Add `Rails::Application.config_for` to load a configuration for the current
environment.
- *Guillermo Iguaran*
-
-* Add `ENV['DATABASE_URL']` support in `rails dbconsole`. Fixes #13320.
-
- *Huiming Teo*
-
-* Add `Application#message_verifier` method to return a message verifier.
-
- This verifier can be used to generate and verify signed messages in the application.
-
- message = Rails.application.message_verifier(:sensitive_data).generate('my sensible data')
- Rails.application.message_verifier(:sensitive_data).verify(message)
- # => 'my sensible data'
+ # config/exception_notification.yml:
+ production:
+ url: http://127.0.0.1:8080
+ namespace: my_app_production
+ development:
+ url: http://localhost:3001
+ namespace: my_app_development
- It is recommended not to use the same verifier for different things, so you can get different
- verifiers passing the name argument.
+ # config/production.rb
+ MyApp::Application.configure do
+ config.middleware.use ExceptionNotifier, config_for(:exception_notification)
+ end
- message = Rails.application.message_verifier(:cookies).generate('my sensible cookie data')
+ *Rafael Mendonça França*, *DHH*
- See the `ActiveSupport::MessageVerifier` documentation for more information.
+* Deprecate `Rails::Rack::LogTailer` without replacement.
*Rafael Mendonça França*
-* 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.
-
- *Jon Leighton*
-
-* Uses .railsrc while creating new plugin if it is available.
- Fixes #10700.
-
- *Prathamesh Sonpatki*
-
-* Remove turbolinks when generating a new application based on a template that skips it.
-
- Example:
-
- Skips turbolinks adding `add_gem_entry_filter { |gem| gem.name != "turbolinks" }`
- to the template.
-
- *Lauro Caetano*
-
-* Instrument an `load_config_initializer.railties` event on each load of configuration initializer
- from `config/initializers`. Subscribers should be attached before `load_config_initializers`
- initializer completed.
-
- Registering subscriber examples:
+* Add a generic --skip-gems options to generator
- # config/application.rb
- module RailsApp
- class Application < Rails::Application
- ActiveSupport::Notifications.subscribe('load_config_initializer.railties') do |*args|
- event = ActiveSupport::Notifications::Event.new(*args)
- puts "Loaded initializer #{event.payload[:initializer]} (#{event.duration}ms)"
- end
- end
- end
-
- # my_engine/lib/my_engine/engine.rb
- module MyEngine
- class Engine < ::Rails::Engine
- config.before_initialize do
- ActiveSupport::Notifications.subscribe('load_config_initializer.railties') do |*args|
- event = ActiveSupport::Notifications::Event.new(*args)
- puts "Loaded initializer #{event.payload[:initializer]} (#{event.duration}ms)"
- end
- end
- end
- end
-
- *Paul Nikitochkin*
-
-* Support for Pathnames in eager load paths.
-
- *Mike Pack*
+ This option is useful if users want to remove some gems like jbuilder,
+ turbolinks, coffee-rails, etc that don't have specific options on the
+ generator.
-* Fixed missing line and shadow on service pages(404, 422, 500).
+ rails new my_app --skip-gems turbolinks coffee-rails
- *Dmitry Korotkov*
+ *Rafael Mendonça França*
-* `BACKTRACE` environment variable to show unfiltered backtraces for
- test failures.
+* Invalid `bin/rails generate` commands will now show spelling suggestions.
- Example:
+ *Richard Schneeman*
- $ BACKTRACE=1 ruby -Itest ...
- # or with rake
- $ BACKTRACE=1 bin/rake
+* Add `bin/setup` script to bootstrap an application.
*Yves Senn*
-* Removal of all javascript stuff (gems and files) when generating a new
- application using the `--skip-javascript` option.
+* Replace double quotes with single quotes while adding an entry into Gemfile.
- *Robin Dupret*
+ *Alexander Belaev*
-* Make the application name snake cased when it contains spaces
+* Default `config.assets.digest` to `true` in development.
- The application name is used to fill the `database.yml` and
- `session_store.rb` files ; previously, if the provided name
- contained whitespaces, it led to unexpected names in these files.
+ *Dan Kang*
- *Robin Dupret*
+* Load database configuration from the first `database.yml` available in paths.
-* Added `--model-name` option to `ScaffoldControllerGenerator`.
+ *Pier-Olivier Thibault*
- *yalab*
+* Reading name and email from git for plugin gemspec.
-* Expose MiddlewareStack#unshift to environment configuration.
+ Fixes #9589.
- *Ben Pickles*
+ *Arun Agrawal*, *Abd ar-Rahman Hamidi*, *Roman Shmatov*
-* `rails server` will only extend the logger to output to STDOUT
- in development environment.
+* Fix `console` and `generators` blocks defined at different environments.
- *Richard Schneeman*
-
-* Don't require passing path to app before options in `rails new`
- and `rails plugin new`
-
- *Piotr Sarnacki*
-
-* rake notes now searches *.less files
-
- *Josh Crowder*
-
-* Generate nested route for namespaced controller generated using
- `rails g controller`.
- Fixes #11532.
-
- Example:
-
- rails g controller admin/dashboard index
-
- # Before:
- get "dashboard/index"
-
- # After:
- namespace :admin do
- get "dashboard/index"
- end
-
- *Prathamesh Sonpatki*
-
-* Fix the event name of action_dispatch requests.
+ Fixes #14748.
*Rafael Mendonça França*
-* Make `config.log_level` work with custom loggers.
-
- *Max Shytikov*
-
-* Changed stylesheet load order in the stylesheet manifest generator.
- Fixes #11639.
-
- *Pawel Janiak*
-
-* Added generated unit test for generator generator using new
- `test:generators` rake task.
+* Move configuration of asset precompile list and version to an initializer.
- *Josef Šimánek*
+ *Matthew Draper*
-* Removed `update:application_controller` rake task.
+* Remove sqlite3 lines from `.gitignore` if the application is not using sqlite3.
- *Josef Šimánek*
+ *Dmitrii Golub*
-* Fix `rake environment` to do not eager load modules
+* Add public API to register new extensions for `rake notes`.
- *Paul Nikitochkin*
+ Example:
-* Fix `rake notes` to look into `*.sass` files
+ config.annotations.register_extensions("scss", "sass") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
- *Yuri Artemev*
+ *Roberto Miranda*
-* Removed deprecated `Rails.application.railties.engines`.
+* Removed unnecessary `rails application` command.
*Arun Agrawal*
-* Removed deprecated threadsafe! from Rails Config.
-
- *Paul Nikitochkin*
-
-* Remove deprecated `ActiveRecord::Generators::ActiveModel#update_attributes` in
- favor of `ActiveRecord::Generators::ActiveModel#update`.
-
- *Vipul A M*
-
-* Remove deprecated `config.whiny_nils` option.
+* Make the `rails:template` rake task load the application's initializers.
- *Vipul A M*
+ Fixes #12133.
-* Rename `commands/plugin_new.rb` to `commands/plugin.rb` and fix references
-
- *Richard Schneeman*
-
-* Fix `rails plugin --help` command.
-
- *Richard Schneeman*
-
-* Omit turbolinks configuration completely on skip_javascript generator option.
-
- *Nikita Fedyashev*
-
-* Removed deprecated rake tasks for running tests: `rake test:uncommitted` and
- `rake test:recent`.
+ *Robin Dupret*
- *John Wang*
+* Introduce `Rails.gem_version` as a convenience method to return
+ `Gem::Version.new(Rails.version)`, suggesting a more reliable way to perform
+ version comparison.
-* Clearing autoloaded constants triggers routes reloading.
- Fixes #10685.
+ Example:
- *Xavier Noria*
+ Rails.version #=> "4.1.2"
+ Rails.gem_version #=> #<Gem::Version "4.1.2">
-* Fixes bug with scaffold generator with `--assets=false --resource-route=false`.
- Fixes #9525.
+ 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
- *Arun Agrawal*
+ *Prem Sichanugrist*
-* 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.
+* 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/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
index eccdee7b07..8d847eaa1c 100644
--- a/railties/RDOC_MAIN.rdoc
+++ b/railties/RDOC_MAIN.rdoc
@@ -1,7 +1,7 @@
== Welcome to \Rails
\Rails is a web-application framework that includes everything needed to create
-database-backed web applications according to the {Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller] pattern.
+database-backed web applications according to the {Model-View-Controller (MVC)}[http://en.wikipedia.org/wiki/Model-view-controller] pattern.
Understanding the MVC pattern is key to understanding \Rails. MVC divides your application
into three layers, each with a specific responsibility.
diff --git a/railties/README.rdoc b/railties/README.rdoc
index 6248b5feed..a25658668c 100644
--- a/railties/README.rdoc
+++ b/railties/README.rdoc
@@ -31,7 +31,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index be7570a5ba..e7172e491f 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -29,7 +29,13 @@ module Rails
autoload :WelcomeController
class << self
- attr_accessor :application, :cache, :logger
+ @application = @app_class = nil
+
+ attr_writer :application
+ attr_accessor :app_class, :cache, :logger
+ def application
+ @application ||= (app_class.instance if app_class)
+ end
delegate :initialize!, :initialized?, to: :application
@@ -80,10 +86,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/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb
index 56f05b3844..39d8007333 100644
--- a/railties/lib/rails/app_rails_loader.rb
+++ b/railties/lib/rails/app_rails_loader.rb
@@ -2,6 +2,8 @@ require 'pathname'
module Rails
module AppRailsLoader
+ extend self
+
RUBY = Gem.ruby
EXECUTABLES = ['bin/rails', 'script/rails']
BUNDLER_WARNING = <<EOS
@@ -26,7 +28,7 @@ generate it and add it to source control:
EOS
- def self.exec_app_rails
+ def exec_app_rails
original_cwd = Dir.pwd
loop do
@@ -54,7 +56,7 @@ EOS
end
end
- def self.find_executable
+ def find_executable
EXECUTABLES.find { |exe| File.file?(exe) }
end
end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index e37347b576..61639be7c6 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -87,7 +87,15 @@ module Rails
class << self
def inherited(base)
super
- Rails.application ||= base.instance
+ Rails.app_class = base
+ end
+
+ def instance
+ super.run_load_hooks!
+ end
+
+ def create(initial_variable_values = {}, &block)
+ new(initial_variable_values, &block).run_load_hooks!
end
# Makes the +new+ method public.
@@ -116,22 +124,33 @@ module Rails
@ordered_railties = nil
@railties = nil
@message_verifiers = {}
+ @ran_load_hooks = false
+
+ # are these actually used?
+ @initial_variable_values = initial_variable_values
+ @block = block
add_lib_to_load_path!
+ end
+
+ # Returns true if the application is initialized.
+ def initialized?
+ @initialized
+ end
+
+ def run_load_hooks! # :nodoc:
+ return self if @ran_load_hooks
+ @ran_load_hooks = true
ActiveSupport.run_load_hooks(:before_configuration, self)
- initial_variable_values.each do |variable_name, value|
+ @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.
- def initialized?
- @initialized
+ instance_eval(&@block) if @block
+ self
end
# Implements call according to the Rack API. It simply
@@ -151,14 +170,13 @@ 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
+ @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
end
# Returns a message verifier object.
@@ -186,6 +204,38 @@ module Rails
end
end
+ # Convenience for loading config/foo.yml for the current Rails env.
+ #
+ # Example:
+ #
+ # # config/exception_notification.yml:
+ # production:
+ # url: http://127.0.0.1:8080
+ # namespace: my_app_production
+ # development:
+ # url: http://localhost:3001
+ # namespace: my_app_development
+ #
+ # # config/production.rb
+ # MyApp::Application.configure do
+ # config.middleware.use ExceptionNotifier, config_for(:exception_notification)
+ # end
+ def config_for(name)
+ yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml")
+
+ if yaml.exist?
+ require "yaml"
+ require "erb"
+ (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {}
+ else
+ raise "Could not load configuration. No such file - #{yaml}"
+ end
+ rescue Psych::SyntaxError => e
+ raise "YAML syntax error occurred while parsing #{yaml}. " \
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
+ "Error: #{e.message}"
+ end
+
# Stores some of the Rails initial environment parameters which
# will be used by middlewares and engines to configure themselves.
def env_config
@@ -230,6 +280,18 @@ module Rails
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)
@@ -332,6 +394,29 @@ 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
+
+ def migration_railties # :nodoc:
+ (ordered_railties & railties_without_main_app).reverse
+ end
+
protected
alias :build_middleware_stack :app
@@ -362,6 +447,11 @@ module Rails
super
end
+ def railties_without_main_app # :nodoc:
+ @railties_without_main_app ||= Rails::Railtie.subclasses.map(&:instance) +
+ Rails::Engine.subclasses.map(&:instance)
+ end
+
# Returns the ordered railties for this application considering railties_order.
def ordered_railties #:nodoc:
@ordered_railties ||= begin
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 33bcab1e57..a26d41c0cf 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -53,11 +53,7 @@ INFO
logger
end
- if ::Logger === Rails.logger
- Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
- else
- Rails.logger.level = config.log_level
- 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 20e3de32aa..782bc4b0f1 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
@@ -12,7 +13,7 @@ module Rails
:railties_order, :relative_url_root, :secret_key_base, :secret_token,
:serve_static_assets, :ssl_options, :static_cache_control, :session_options,
:time_zone, :reload_classes_only_on_change,
- :beginning_of_week, :filter_redirect
+ :beginning_of_week, :filter_redirect, :x
attr_writer :log_level
attr_reader :encoding
@@ -47,6 +48,7 @@ module Rails
@eager_load = nil
@secret_token = nil
@secret_key_base = nil
+ @x = Custom.new
@assets = ActiveSupport::OrderedOptions.new
@assets.enabled = true
@@ -91,9 +93,10 @@ module Rails
# Loads and returns the entire raw configuration of database from
# values stored in `config/database.yml`.
def database_configuration
- yaml = Pathname.new(paths["config/database"].first || "")
+ yaml = Pathname.new(paths["config/database"].existent.first || "")
config = if yaml.exist?
+ require "yaml"
require "erb"
YAML.load(ERB.new(yaml.read).result) || {}
elsif ENV['DATABASE_URL']
@@ -149,6 +152,20 @@ module Rails
end
end
+ def annotations
+ SourceAnnotationExtractor::Annotation
+ end
+
+ private
+ class Custom
+ def initialize
+ @configurations = Hash.new
+ end
+
+ def method_missing(method, *args)
+ @configurations[method] ||= ActiveSupport::OrderedOptions.new
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 5b8509b2e9..7a1bb1e25c 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -22,8 +22,6 @@ 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/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb
index de60423784..6cfbc70c51 100644
--- a/railties/lib/rails/commands/commands_tasks.rb
+++ b/railties/lib/rails/commands/commands_tasks.rb
@@ -20,7 +20,6 @@ The most common rails commands are:
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")
@@ -28,7 +27,7 @@ In addition to those, there are:
All commands can be run with -h (or --help) for more information.
EOT
- COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
+ COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help)
def initialize(argv)
@argv = argv
@@ -87,10 +86,6 @@ EOT
Rails::DBConsole.start
end
- def application
- require_command!("application")
- end
-
def runner
require_command!("runner")
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 f6d8aec30d..1a2613a8d0 100644
--- a/railties/lib/rails/commands/dbconsole.rb
+++ b/railties/lib/rails/commands/dbconsole.rb
@@ -20,7 +20,7 @@ module Rails
ENV['RAILS_ENV'] = options[:environment] || environment
case config["adapter"]
- when /^mysql/
+ when /^(jdbc)?mysql/
args = {
'host' => '--host',
'port' => '--port',
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
index f7a0b99005..95bbdd4cdf 100644
--- a/railties/lib/rails/commands/plugin.rb
+++ b/railties/lib/rails/commands/plugin.rb
@@ -11,7 +11,7 @@ else
end
if File.exist?(railsrc)
extra_args_string = File.read(railsrc)
- extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten
+ 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
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index fec4962fb5..c3b7bb6f84 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -9,7 +9,17 @@ module Rails
def parse!(args)
args, options = args.dup, {}
- opt_parser = OptionParser.new do |opts|
+ option_parser(options).parse! args
+
+ options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
+ options[:server] = args.shift
+ options
+ end
+
+ private
+
+ def option_parser(options)
+ OptionParser.new do |opts|
opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
opts.on("-p", "--port=port", Integer,
"Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
@@ -18,7 +28,14 @@ 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 }
@@ -30,12 +47,6 @@ module Rails
opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
end
-
- opt_parser.parse! args
-
- options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
- options[:server] = args.shift
- options
end
end
@@ -75,7 +86,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.
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 5c54cdaa70..dc3da1eb41 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -371,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
@@ -395,7 +395,7 @@ module Rails
end
unless mod.respond_to?(:railtie_routes_url_helpers)
- define_method(:railtie_routes_url_helpers) { railtie.routes.url_helpers }
+ define_method(:railtie_routes_url_helpers) {|include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) }
end
end
end
@@ -429,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)
@@ -510,7 +509,7 @@ module Rails
def call(env)
env.merge!(env_config)
if env['SCRIPT_NAME']
- env.merge! "ROUTES_#{routes.object_id}_SCRIPT_NAME" => env['SCRIPT_NAME'].dup
+ env["ROUTES_#{routes.object_id}_SCRIPT_NAME"] = env['SCRIPT_NAME'].dup
end
app.call(env)
end
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 dce734b54e..bf2390cb7e 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -156,10 +156,20 @@ module Rails
args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
klass.start(args, config)
else
- puts "Could not find generator #{namespace}."
+ options = sorted_groups.map(&:last).flatten
+ suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
+ msg = "Could not find generator '#{namespace}'. "
+ msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.join(" or ") }\n"
+ msg << "Run `rails generate --help` for more options."
+ puts msg
end
end
+ # Returns an array of generator namespaces that are hidden.
+ # Generator namespaces may be hidden for a variety of reasons.
+ # Some are aliased such as "rails:migration" and can be
+ # invoked with the shorter "migration", others are private to other generators
+ # such as "css:scaffold".
def self.hidden_namespaces
@hidden_namespaces ||= begin
orm = options[:rails][:orm]
@@ -199,17 +209,6 @@ module Rails
# Show help message with available generators.
def self.help(command = 'generate')
- lookup!
-
- namespaces = subclasses.map{ |k| k.namespace }
- namespaces.sort!
-
- groups = Hash.new { |h,k| h[k] = [] }
- namespaces.each do |namespace|
- base = namespace.split(':').first
- groups[base] << namespace
- end
-
puts "Usage: rails #{command} GENERATOR [args] [options]"
puts
puts "General options:"
@@ -222,20 +221,74 @@ module Rails
puts "Please choose a generator below."
puts
- # Print Rails defaults first.
+ print_generators
+ end
+
+ def self.public_namespaces
+ lookup!
+ subclasses.map { |k| k.namespace }
+ end
+
+ def self.print_generators
+ sorted_groups.each { |b, n| print_list(b, n) }
+ end
+
+ def self.sorted_groups
+ namespaces = public_namespaces
+ namespaces.sort!
+ groups = Hash.new { |h,k| h[k] = [] }
+ namespaces.each do |namespace|
+ base = namespace.split(':').first
+ groups[base] << namespace
+ end
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, '') }
rails.delete("app")
rails.delete("plugin")
- print_list("rails", rails)
hidden_namespaces.each { |n| groups.delete(n.to_s) }
- groups.sort.each { |b, n| print_list(b, n) }
+ [["rails", rails]] + groups.sort.to_a
end
protected
+ # This code is based directly on the Text gem implementation
+ # Returns a value representing the "cost" of transforming str1 into str2
+ def self.levenshtein_distance str1, str2
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+ max = n/2
+
+ return m if (0 == n)
+ return n if (0 == m)
+ return n if (n - m).abs > max
+
+ d = (0..m).to_a
+ x = nil
+
+ str1.each_char.each_with_index do |char1,i|
+ e = i+1
+
+ str2.each_char.each_with_index do |char2,j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j+1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
+ end
+
+ d[m] = x
+ end
+
+ return x
+ end
+
# Prints a list of generators.
def self.print_list(base, namespaces) #:nodoc:
namespaces = namespaces.reject do |n|
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index afdbf5c241..4709914947 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -7,6 +7,7 @@ module Rails
def initialize(*) # :nodoc:
super
@in_group = nil
+ @after_bundle_callbacks = []
end
# Adds an entry into +Gemfile+ for the supplied gem.
@@ -20,9 +21,9 @@ module Rails
# Set the message to be shown in logs. Uses the git repo if one is given,
# otherwise use name (version).
- parts, message = [ name.inspect ], name
+ parts, message = [ quote(name) ], name
if version ||= options.delete(:version)
- parts << version.inspect
+ parts << quote(version)
message << " (#{version})"
end
message = options[:git] if options[:git]
@@ -30,7 +31,7 @@ module Rails
log :gemfile, message
options.each do |option, value|
- parts << "#{option}: #{value.inspect}"
+ parts << "#{option}: #{quote(value)}"
end
in_root do
@@ -68,7 +69,7 @@ module Rails
log :source, source
in_root do
- prepend_file "Gemfile", "source #{source.inspect}\n", verbose: false
+ prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
end
end
@@ -188,7 +189,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
@@ -232,6 +233,16 @@ module Rails
log File.read(find_in_source_paths(path))
end
+ # Registers a callback to be executed after bundle and spring binstubs
+ # have run.
+ #
+ # after_bundle do
+ # git add: '.'
+ # end
+ def after_bundle(&block)
+ @after_bundle_callbacks << block
+ end
+
protected
# Define log for backwards compatibility. If just one argument is sent,
@@ -255,6 +266,15 @@ module Rails
end
end
+ # Surround string with single quotes if there is no quotes.
+ # Otherwise fall back to double quotes
+ def quote(str)
+ if str.include?("'")
+ str.inspect
+ else
+ "'#{str}'"
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb
index 9c3332927f..682092fdf2 100644
--- a/railties/lib/rails/generators/actions/create_migration.rb
+++ b/railties/lib/rails/generators/actions/create_migration.rb
@@ -1,4 +1,4 @@
-require 'thor/actions/create_file'
+require 'thor/actions'
module Rails
module Generators
@@ -55,7 +55,8 @@ module Rails
else
say_status :conflict, :red
raise Error, "Another migration is already named #{migration_file_name}: " +
- "#{existing_migration}. Use --force to replace this migration file."
+ "#{existing_migration}. Use --force to replace this migration " +
+ "or --skip to ignore conflicted file."
end
end
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index f1f79d8378..caaaae09e6 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -41,6 +41,9 @@ module Rails
class_option :skip_active_record, type: :boolean, aliases: '-O', default: false,
desc: 'Skip Active Record files'
+ class_option :skip_gems, type: :array, default: [],
+ desc: 'Skip the provided gems files'
+
class_option :skip_action_view, type: :boolean, aliases: '-V', default: false,
desc: 'Skip Action View files'
@@ -79,8 +82,7 @@ module Rails
end
def initialize(*args)
- @original_wd = Dir.pwd
- @gem_filter = lambda { |gem| true }
+ @gem_filter = lambda { |gem| !options[:skip_gems].include?(gem.name) }
@extra_entries = []
super
convert_database_option_for_jruby
@@ -105,15 +107,14 @@ module Rails
end
def gemfile_entries
- [ rails_gemfile_entry,
- database_gemfile_entry,
- assets_gemfile_entry,
- javascript_gemfile_entry,
- jbuilder_gemfile_entry,
- sdoc_gemfile_entry,
- platform_dependent_gemfile_entry,
- spring_gemfile_entry,
- @extra_entries].flatten.find_all(&@gem_filter)
+ [rails_gemfile_entry,
+ database_gemfile_entry,
+ assets_gemfile_entry,
+ javascript_gemfile_entry,
+ jbuilder_gemfile_entry,
+ sdoc_gemfile_entry,
+ psych_gemfile_entry,
+ @extra_entries].flatten.find_all(&@gem_filter)
end
def add_gem_entry_filter
@@ -173,6 +174,10 @@ module Rails
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
@@ -189,19 +194,19 @@ module Rails
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?
[GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH),
- GemfileEntry.github('arel', 'rails/arel')]
+ GemfileEntry.github('arel', 'rails/arel'),
+ GemfileEntry.github('rack', 'rack/rack'),
+ GemfileEntry.github('i18n', 'svenfuchs/i18n')]
elsif options.edge?
[GemfileEntry.github('rails', 'rails/rails'),
- GemfileEntry.github('arel', 'rails/arel')]
+ GemfileEntry.github('arel', 'rails/arel'),
+ GemfileEntry.github('rack', 'rack/rack'),
+ GemfileEntry.github('i18n', 'svenfuchs/i18n')]
else
[GemfileEntry.version('rails',
Rails::VERSION::STRING,
@@ -247,7 +252,7 @@ module Rails
'Use SCSS for stylesheets')
else
gems << GemfileEntry.version('sass-rails',
- '~> 4.0.1',
+ '~> 4.0.3',
'Use SCSS for stylesheets')
end
@@ -258,14 +263,6 @@ module Rails
gems
end
- def platform_dependent_gemfile_entry
- gems = []
- if RUBY_ENGINE == 'rbx'
- gems << GemfileEntry.version('rubysl', nil)
- end
- gems
- end
-
def jbuilder_gemfile_entry
comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder'
GemfileEntry.version('jbuilder', '~> 2.0', comment)
@@ -308,10 +305,12 @@ module Rails
end
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)
+ def psych_gemfile_entry
+ return [] unless defined?(Rubinius)
+
+ comment = 'Use Psych as the YAML engine, instead of Syck, so serialized ' \
+ 'data can be read safely from different rubies (see http://git.io/uuLVag)'
+ GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx)
end
def bundle_command(command)
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 67bab96a22..9af6435f23 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -83,7 +83,7 @@ module Rails
#
# The first and last part used to find the generator to be invoked are
# guessed based on class invokes hook_for, as noticed in the example above.
- # This can be customized with two options: :base and :as.
+ # This can be customized with two options: :in and :as.
#
# Let's suppose you are creating a generator that needs to invoke the
# controller generator from test unit. Your first attempt is:
@@ -108,7 +108,7 @@ module Rails
# "test_unit:controller", "test_unit"
#
# Similarly, if you want it to also lookup in the rails namespace, you just
- # need to provide the :base value:
+ # need to provide the :in value:
#
# class AwesomeGenerator < Rails::Generators::Base
# hook_for :test_framework, in: :rails, as: :controller
diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb
index cfd77097d5..0755ac335c 100644
--- a/railties/lib/rails/generators/erb.rb
+++ b/railties/lib/rails/generators/erb.rb
@@ -6,7 +6,7 @@ module Erb # :nodoc:
protected
def formats
- format
+ [format]
end
def format
@@ -17,7 +17,7 @@ module Erb # :nodoc:
:erb
end
- def filename_with_extensions(name, format)
+ 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 e62aece7c5..94c1b835d1 100644
--- a/railties/lib/rails/generators/erb/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb
@@ -11,7 +11,7 @@ module Erb # :nodoc:
actions.each do |action|
@action = action
- Array(formats).each do |format|
+ formats.each do |format|
@path = File.join(base_path, filename_with_extensions(action, format))
template filename_with_extensions(:view, format), @path
end
diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
index b219f459ac..c94829a0ae 100644
--- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
@@ -14,7 +14,7 @@ module Erb # :nodoc:
def copy_view_files
available_views.each do |view|
- Array(formats).each do |format|
+ formats.each do |format|
filename = filename_with_extensions(view, format)
template filename, File.join("app/views", controller_file_path, filename)
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 69c10efa47..bba9141fb8 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>
@@ -17,17 +17,12 @@
<%%= f.label :password %><br>
<%%= f.password_field :password %>
</div>
- <div>
+ <div class="field">
<%%= f.label :password_confirmation %><br>
<%%= f.password_field :password_confirmation %>
<% else -%>
- <%- if attribute.reference? -%>
<%%= f.label :<%= attribute.column_name %> %><br>
<%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %>
- <%- else -%>
- <%%= f.label :<%= attribute.name %> %><br>
- <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
- <%- end -%>
<% end -%>
</div>
<% end -%>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
index e58b9fbd08..5620fcc850 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
@@ -1,4 +1,4 @@
-<h1>Editing <%= singular_table_name %></h1>
+<h1>Editing <%= singular_table_name.titleize %></h1>
<%%= render 'form' %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
index 814d6fdb0e..5e194783ff 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
@@ -1,4 +1,6 @@
-<h1>Listing <%= plural_table_name %></h1>
+<p id="notice"><%%= notice %></p>
+
+<h1>Listing <%= plural_table_name.titleize %></h1>
<table>
<thead>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
index 02ae4d015e..db13a5d870 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
@@ -1,4 +1,4 @@
-<h1>New <%= singular_table_name %></h1>
+<h1>New <%= singular_table_name.titleize %></h1>
<%%= render 'form' %>
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/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 5a92ab3e95..b7da44ca2d 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -30,7 +30,12 @@ module Rails
protected
attr_reader :file_name
- alias :singular_name :file_name
+
+ # FIXME: We are avoiding to use alias because a bug on thor that make
+ # this method public and add it to the task list.
+ def singular_name
+ file_name
+ end
# Wrap block with namespace of current application
# if namespace exists and is not skipped
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index 83cb1dc0d5..9110c129d1 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
@@ -86,6 +86,16 @@ module Rails
end
end
+ def config_when_updating
+ cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb')
+
+ config
+
+ unless cookie_serializer_config_exist
+ gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal'
+ end
+ end
+
def database_yml
template "config/databases/#{options[:database]}.yml", "config/database.yml"
end
@@ -188,6 +198,11 @@ module Rails
build(:config)
end
+ def update_config_files
+ build(:config_when_updating)
+ end
+ remove_task :update_config_files
+
def create_boot_file
template "config/boot.rb"
end
@@ -231,6 +246,12 @@ module Rails
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
@@ -238,6 +259,12 @@ module Rails
public_task :apply_rails_template, :run_bundle
public_task :generate_spring_binstubs
+ def run_after_bundle_callbacks
+ @after_bundle_callbacks.each do |callback|
+ callback.call
+ end
+ end
+
protected
def self.banner
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index 6d017e187d..8b51fda359 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -1,6 +1,5 @@
source 'https://rubygems.org'
-<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
<% gemfile_entries.each do |gem| -%>
<% if gem.comment -%>
@@ -8,26 +7,39 @@ source 'https://rubygems.org'
<% 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|
+, <%= gem.options.map { |k,v|
"#{k}: #{v.inspect}" }.join(', ') %>
<% end -%>
<% end -%>
# Use ActiveModel has_secure_password
-# gem 'bcrypt-ruby', '~> 3.1.2'
+# gem 'bcrypt', '~> 3.1.7'
-# Use unicorn as the app server
+# Use Unicorn as the app server
# gem 'unicorn'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
+group :development, :test do
<% unless defined?(JRUBY_VERSION) -%>
-# Use debugger
-# gem 'debugger', group: [:development, :test]
+ # Call 'debugger' anywhere in the code to stop execution and get a debugger console
+ <%- if RUBY_VERSION < '2.0.0' -%>
+ gem 'debugger'
+ <%- else -%>
+ gem 'byebug'
+ <%- end -%>
+
+ # Access an IRB console on exceptions page and /console in development
+ gem 'web-console'
+<%- if spring_install? %>
+ # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
+ gem 'spring'
+<% end -%>
<% 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]
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw]
<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup
new file mode 100644
index 0000000000..0e22b3fa5c
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup
@@ -0,0 +1,28 @@
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+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 5e5f0c1fac..6b750f00b1 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,3 @@
-# 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 'bundler/setup' # Set up gems listed in the 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 138e3a8664..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
@@ -23,7 +23,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
index 2cdb592eeb..187ff01bac 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
@@ -1,7 +1,7 @@
# IBM Dataservers
#
# Home Page
-# http://rubyforge.org/projects/rubyibm/
+# https://github.com/dparnell/ibm_db
#
# To install the ibm_db gem:
#
@@ -31,8 +31,6 @@
# Configure Using Gemfile
# gem 'ibm_db'
#
-# 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
#
default: &default
adapter: ibm_db
@@ -61,7 +59,27 @@ test:
<<: *default
database: <%= app_name[0,4] %>_tst
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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 cefd30d519..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
@@ -53,7 +53,16 @@ test:
<<: *default
url: jdbc:db://localhost/<%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ 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 d31761349c..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
@@ -26,6 +26,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
-production: <%%= ENV["DATABASE_URL"] %>
+# 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:
+ <<: *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/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
index 0d248dc197..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
@@ -42,7 +42,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
index 66eba3bf0d..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
@@ -18,7 +18,6 @@ test:
<<: *default
database: db/test.sqlite3
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
production:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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 d618fc28a6..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
@@ -32,11 +32,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Avoid production credentials in the repository,
-# instead read the configuration from the environment.
+# 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.
#
-# Example:
-# mysql2://myuser:mypass@localhost/somedatabase
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
index d469ec0f99..9aedcc15cb 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
@@ -1,7 +1,7 @@
# Oracle/OCI 8i, 9, 10g
#
# Requires Ruby/OCI8:
-# http://rubyforge.org/projects/ruby-oci8/
+# https://github.com/kubo/ruby-oci8
#
# Specify your database using any valid connection syntax, such as a
# tnsnames.ora service name, or an SQL connect string of the form:
@@ -32,7 +32,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
index 93f48656b2..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
@@ -59,11 +59,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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.
#
-# Example:
-# postgres://myuser:mypass@localhost/somedatabase
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
index 7312ddb6cd..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
@@ -20,11 +20,6 @@ test:
<<: *default
database: db/test.sqlite3
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
-#
-# Example:
-# sqlite3://myuser:mypass@localhost/full/path/to/somedatabase
-#
production:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *default
+ database: db/production.sqlite3
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 aa960e493e..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
@@ -42,7 +42,27 @@ test:
<<: *default
database: <%= app_name %>_test
-# Do not keep production credentials in the repository,
-# instead read the configuration from the environment.
+# 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:
- url: <%%= ENV["DATABASE_URL"] %>
+ <<: *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/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
index de12565a73..35e3035a0b 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
@@ -30,6 +30,10 @@ Rails.application.configure do
# number of complex assets.
config.assets.debug = true
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
+ config.assets.digest = true
+
# Adds additional error checking when serving assets at runtime.
# Checks for improperly declared sprockets dependencies.
# Raises helpful error messages.
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 d9cc60d656..277fe01e89 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
@@ -16,10 +16,10 @@ Rails.application.configure do
# Enable Rack::Cache to put a simple HTTP cache in front of your application
# Add `rack-cache` to your Gemfile before enabling this.
- # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
+ # For large-scale production use, consider using a caching reverse proxy like NGINX, varnish or squid.
# config.action_dispatch.rack_cache = true
- # Disable Rails's static asset server (Apache or nginx will already do this).
+ # Disable Rails's static asset server (Apache or NGINX will already do this).
config.serve_static_assets = false
<%- unless options.skip_sprockets? -%>
@@ -30,22 +30,22 @@ Rails.application.configure do
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # Generate digests for assets URLs.
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
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` and `config.assets.version` have moved to config/initializers/assets.rb
<%- end -%>
# Specifies the header that your server uses for sending files.
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
- # Set to :debug to see everything in the log.
- config.log_level = :info
+ # Set to :info to decrease the log volume.
+ config.log_level = :debug
# Prepend all log lines with the following tags.
# config.log_tags = [ :subdomain, :uuid ]
@@ -59,12 +59,6 @@ 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
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..01ef3e6630
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
@@ -0,0 +1,11 @@
+# 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'
+
+# Add additional assets to the asset load path
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# 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
index 7a06a89f0f..7f70458dee 100644
--- 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
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.action_dispatch.cookies_serializer = :json \ No newline at end of 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/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/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
index 6b011e577a..87b8fe3516 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
@@ -5,9 +5,6 @@ require 'rails/test_help'
class ActiveSupport::TestCase
<% unless options[:skip_active_record] -%>
# 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
- # -- they do not yet inherit this setting
fixtures :all
<% end -%>
diff --git a/railties/lib/rails/generators/rails/controller/USAGE b/railties/lib/rails/generators/rails/controller/USAGE
index de33900e0a..64239ad599 100644
--- a/railties/lib/rails/generators/rails/controller/USAGE
+++ b/railties/lib/rails/generators/rails/controller/USAGE
@@ -16,4 +16,3 @@ Example:
Test: test/controllers/credit_cards_controller_test.rb
Views: app/views/credit_cards/debit.html.erb [...]
Helper: app/helpers/credit_cards_helper.rb
- Test: test/helpers/credit_cards_helper_test.rb
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index 33a0d81bf6..fbecab1823 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -2,6 +2,8 @@ module Rails
module Generators
class ControllerGenerator < NamedBase # :nodoc:
argument :actions, type: :array, default: [], banner: "action action"
+ class_option :skip_routes, type: :boolean, desc: "Dont' add routes to config/routes.rb."
+
check_class_collision suffix: "Controller"
def create_controller_files
@@ -9,8 +11,10 @@ module Rails
end
def add_routes
- actions.reverse.each do |action|
- route generate_routing_code(action)
+ unless options[:skip_routes]
+ actions.reverse.each do |action|
+ route generate_routing_code(action)
+ end
end
end
@@ -27,11 +31,11 @@ module Rails
# end
# end
def generate_routing_code(action)
- depth = class_path.length
+ depth = regular_class_path.length
# Create 'namespace' ladder
# namespace :foo do
# namespace :bar do
- namespace_ladder = class_path.each_with_index.map do |ns, i|
+ namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
indent("namespace :#{ns} do\n", i * 2)
end.join
diff --git a/railties/lib/rails/generators/rails/helper/USAGE b/railties/lib/rails/generators/rails/helper/USAGE
index 30e323a858..8855ef3b01 100644
--- a/railties/lib/rails/generators/rails/helper/USAGE
+++ b/railties/lib/rails/generators/rails/helper/USAGE
@@ -5,13 +5,9 @@ Description:
To create a helper within a module, specify the helper name as a
path like 'parent_module/helper_name'.
- This generates a helper class in app/helpers and invokes the configured
- test framework.
-
Example:
`rails generate helper CreditCard`
Credit card helper.
Helper: app/helpers/credit_card_helper.rb
- Test: test/helpers/credit_card_helper_test.rb
diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE
index 833b7beb7f..2a6b8700e3 100644
--- a/railties/lib/rails/generators/rails/model/USAGE
+++ b/railties/lib/rails/generators/rails/model/USAGE
@@ -6,6 +6,11 @@ Description:
model's attributes. Timestamps are added by default, so you don't have to
specify them by hand as 'created_at:datetime updated_at:datetime'.
+ As a special case, specifying 'password:digest' will generate a
+ password_digest field of string type, and configure your generated model and
+ tests for use with ActiveModel has_secure_password (assuming the default ORM
+ and test framework are being used).
+
You don't have to think up every attribute up front, but it helps to
sketch out a few so you can start working with the model immediately.
@@ -27,7 +32,8 @@ Available field types:
`rails generate model post title:string body:text`
will generate a title column with a varchar type and a body column with a text
- type. You can use the following types:
+ type. If no type is specified the string type will be used by default.
+ You can use the following types:
integer
primary_key
@@ -73,6 +79,10 @@ Available field types:
`rails generate model user username:string{30}:uniq`
`rails generate model product supplier:references{polymorphic}:index`
+ If you require a `password_digest` string column for use with
+ has_secure_password, you should specify `password:digest`:
+
+ `rails generate model user password:digest`
Examples:
`rails generate model account`
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/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index f6f529b80a..584f776c01 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -288,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
@@ -304,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."
diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
index 5fdf0e1554..919c349470 100644
--- a/railties/lib/rails/generators/rails/plugin/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/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
index f0a832f783..35ad9fbf9e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
@@ -1,7 +1,7 @@
-source "https://rubygems.org"
+source 'https://rubygems.org'
<% if options[:skip_gemspec] -%>
-<%= '# ' if options.dev? || options.edge? -%>gem "rails", "~> <%= Rails::VERSION::STRING %>"
+<%= '# ' if options.dev? || options.edge? -%>gem 'rails', '~> <%= Rails::VERSION::STRING %>'
<% else -%>
# Declare your gem's dependencies in <%= name %>.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
@@ -11,7 +11,7 @@ gemspec
<% if options[:skip_gemspec] -%>
group :development do
- gem "<%= gem_for_database %>"
+ gem '<%= gem_for_database %>'
end
<% else -%>
# Declare any dependencies that are still in development here instead of in
@@ -31,13 +31,17 @@ end
<% 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|
+, <%= gem.options.map { |k,v|
"#{k}: #{v.inspect}" }.join(', ') %>
<% end -%>
<% end -%>
<% end -%>
<% unless defined?(JRUBY_VERSION) -%>
-# To use debugger
-# gem 'debugger'
+# To use a debugger
+ <%- if RUBY_VERSION < '2.0.0' -%>
+# gem 'debugger', group: [:development, :test]
+ <%- else -%>
+# gem 'byebug', group: [:development, :test]
+ <%- end -%>
<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
index d7a9109894..ff2fb3ba4e 100644
--- a/railties/lib/rails/generators/rails/plugin/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/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
index 0ba899176c..c338a0bdb1 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile
+++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
@@ -19,6 +19,10 @@ APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'
<% end %>
+<% if engine? -%>
+load 'rails/tasks/statistics.rake'
+<% end %>
+
<% unless options[:skip_gemspec] -%>
Bundler::GemHelper.install_tasks
diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE
index 4a3eb2c7c7..1b2a944103 100644
--- a/railties/lib/rails/generators/rails/scaffold/USAGE
+++ b/railties/lib/rails/generators/rails/scaffold/USAGE
@@ -9,11 +9,16 @@ Description:
Attributes are field arguments specifying the model's attributes. You can
optionally pass the type and an index to each field. For instance:
- "title body:text tracking_id:integer:uniq" will generate a title field of
+ 'title body:text tracking_id:integer:uniq' will generate a title field of
string type, a body with text type and a tracking_id as an integer with an
unique index. "index" could also be given instead of "uniq" if one desires
a non unique index.
+ As a special case, specifying 'password:digest' will generate a
+ password_digest field of string type, and configure your generated model,
+ controller, views, and test suite for use with ActiveModel
+ has_secure_password (assuming they are using Rails defaults).
+
Timestamps are added by default, so you don't have to specify them by hand
as 'created_at:datetime updated_at:datetime'.
@@ -33,3 +38,4 @@ Examples:
`rails generate scaffold post`
`rails generate scaffold post title body:text published:boolean`
`rails generate scaffold purchase amount:decimal tracking_id:integer:uniq`
+ `rails generate scaffold user email:uniq password:digest`
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 0e69aa101f..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
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index 7329ee9f48..4669935156 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -1,14 +1,14 @@
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
@@ -21,15 +21,6 @@ module Rails
assign_names!(self.name)
end
- 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)
- end
-
assign_controller_names!(controller_name.pluralize)
end
diff --git a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
index 0db76f9eaf..bde4e88915 100644
--- a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
+++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
@@ -3,11 +3,7 @@ require 'rails/generators/test_unit'
module TestUnit # :nodoc:
module Generators # :nodoc:
class HelperGenerator < Base # :nodoc:
- check_class_collision suffix: "HelperTest"
-
- def create_helper_files
- template 'helper_test.rb', File.join('test/helpers', class_path, "#{file_name}_helper_test.rb")
- end
+ # Rails does not generate anything here.
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb b/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb
deleted file mode 100644
index 7d37bda0f9..0000000000
--- a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-require 'test_helper'
-
-<% module_namespacing do -%>
-class <%= class_name %>HelperTest < ActionView::TestCase
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb
index 2e877f8762..bd069e4bd0 100644
--- a/railties/lib/rails/generators/testing/assertions.rb
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -1,3 +1,5 @@
+require 'shellwords'
+
module Rails
module Generators
module Testing
diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb
index 7576eba6e0..e0600d0b59 100644
--- a/railties/lib/rails/generators/testing/behaviour.rb
+++ b/railties/lib/rails/generators/testing/behaviour.rb
@@ -100,6 +100,23 @@ module Rails
dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '')
Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
end
end
end
diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb
index edadeaca0e..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_view action_mailer active_support )
+ %w( active_record action_pack action_view action_mailer active_support active_model )
end
def framework_version(framework)
diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb
index 908c4ce65e..49e5431a16 100644
--- a/railties/lib/rails/info_controller.rb
+++ b/railties/lib/rails/info_controller.rb
@@ -5,7 +5,7 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
layout -> { request.xhr? ? false : 'application' }
- before_filter :require_local!
+ before_action :require_local!
def index
redirect_to action: :routes
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index dd318f52e5..32740d66da 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -3,8 +3,8 @@ 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
+ before_action :require_local!
+ before_action :find_preview, only: :preview
def index
@previews = ActionMailer::Preview.all
@@ -70,4 +70,4 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
@email
end
end
-end \ No newline at end of file
+end
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index 117bb37487..3eb66c07af 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -101,7 +101,7 @@ module Rails
def filter_by(&block)
all_paths.find_all(&block).flat_map { |path|
paths = path.existent
- paths - path.children.map { |p| yield(p) ? [] : p.existent }.flatten
+ paths - path.children.flat_map { |p| yield(p) ? [] : p.existent }
}.uniq
end
end
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 50d0eb96fc..bc26421a9e 100644
--- a/railties/lib/rails/rack/log_tailer.rb
+++ b/railties/lib/rails/rack/log_tailer.rb
@@ -1,7 +1,11 @@
+require 'active_support/deprecation'
+
module Rails
module Rack
class LogTailer
def initialize(app, log = nil)
+ ActiveSupport::Deprecation.warn "LogTailer is deprecated and will be removed on Rails 5"
+
@app = app
path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath
diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb
index 3b35798679..9962e6d943 100644
--- a/railties/lib/rails/rack/logger.rb
+++ b/railties/lib/rails/rack/logger.rb
@@ -34,7 +34,7 @@ module Rails
instrumenter = ActiveSupport::Notifications.instrumenter
instrumenter.start 'request.action_dispatch', request: request
- logger.info started_request_message(request)
+ logger.info { started_request_message(request) }
resp = @app.call(env)
resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
resp
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index c63e0c0758..2b33beaa2b 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -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/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb
index 3b7f358a5b..df74643a59 100644
--- a/railties/lib/rails/ruby_version_check.rb
+++ b/railties/lib/rails/ruby_version_check.rb
@@ -2,7 +2,7 @@ if RUBY_VERSION < '1.9.3'
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
- Rails 4 prefers to run on Ruby 2.0.
+ Rails 4 prefers to run on Ruby 2.1 or newer.
You're running
#{desc}
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 3cf6a005ea..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.
@@ -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|sass|less|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/framework.rake b/railties/lib/rails/tasks/framework.rake
index e669315934..a1c805f8aa 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -3,7 +3,7 @@ namespace :rails do
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+\-\.]*://}
@@ -55,7 +55,7 @@ namespace :rails do
# desc "Update config/boot.rb from your current rails install"
task :configs do
invoke_from_app_generator :create_boot_file
- invoke_from_app_generator :create_config_files
+ invoke_from_app_generator :update_config_files
end
# desc "Adds new executables to the application bin/ directory"
diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake
index c1674c72ad..ae5a7d2759 100644
--- a/railties/lib/rails/tasks/statistics.rake
+++ b/railties/lib/rails/tasks/statistics.rake
@@ -1,3 +1,6 @@
+# while having global constant is not good,
+# many 3rd party tools depend on it, like rspec-rails, cucumber-rails, etc
+# so if will be removed - deprecation warning is needed
STATS_DIRECTORIES = [
%w(Controllers app/controllers),
%w(Helpers app/helpers),
@@ -13,10 +16,12 @@ STATS_DIRECTORIES = [
%w(Integration\ tests test/integration),
%w(Functional\ tests\ (old) test/functional),
%w(Unit\ tests \ (old) test/unit)
-].collect { |name, dir| [ name, "#{Rails.root}/#{dir}" ] }.select { |name, dir| File.directory?(dir) }
+].collect do |name, dir|
+ [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ]
+end.select { |name, dir| File.directory?(dir) }
-desc "Report code statistics (KLOCs, etc) from the application"
+desc "Report code statistics (KLOCs, etc) from the application or engine"
task :stats do
require 'rails/code_statistics'
CodeStatistics.new(*STATS_DIRECTORIES).to_s
-end
+end \ No newline at end of file
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index 2e6356343d..c837fadb40 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -4,6 +4,7 @@ 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'
diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb
index d9bffba4d7..6fa96d2ced 100644
--- a/railties/lib/rails/test_unit/sub_test_task.rb
+++ b/railties/lib/rails/test_unit/sub_test_task.rb
@@ -7,7 +7,7 @@ module Rails
#
# This class takes a TestInfo class and defines the appropriate rake task
# based on the information, then invokes it.
- class TestCreator
+ class TestCreator # :nodoc:
def initialize(info)
@info = info
end
@@ -41,7 +41,7 @@ module Rails
# 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
+ class TestInfo # :nodoc:
def initialize(tasks)
@tasks = tasks
@files = nil
diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb
index 923cab4e2a..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 = "beta1"
+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/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index 9ccc286b4e..b6533a5fb2 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -26,3 +26,26 @@ end
def jruby_skip(message = '')
skip message if defined?(JRUBY_VERSION)
end
+
+class ActiveSupport::TestCase
+ private
+
+ unless defined?(:capture)
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+ end
+end
diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb
index 1d3b80253a..d4885447e6 100644
--- a/railties/test/app_rails_loader_test.rb
+++ b/railties/test/app_rails_loader_test.rb
@@ -3,13 +3,27 @@ require 'abstract_unit'
require 'rails/app_rails_loader'
class AppRailsLoaderTest < ActiveSupport::TestCase
+ def loader
+ @loader ||= Class.new do
+ extend Rails::AppRailsLoader
+
+ def self.exec_arguments
+ @exec_arguments
+ end
+
+ def self.exec(*args)
+ @exec_arguments = args
+ end
+ end
+ end
+
def write(filename, contents=nil)
FileUtils.mkdir_p(File.dirname(filename))
File.write(filename, contents)
end
def expects_exec(exe)
- Rails::AppRailsLoader.expects(:exec).with(Rails::AppRailsLoader::RUBY, exe)
+ assert_equal [Rails::AppRailsLoader::RUBY, exe], loader.exec_arguments
end
setup do
@@ -22,30 +36,30 @@ 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(:file?).with('bin/rails').returns(false)
- File.stubs(:file?).with('script/rails').returns(false)
+ def loader.find_executables; end
- assert !Rails::AppRailsLoader.exec_app_rails
+ assert !loader.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
+ assert !loader.exec_app_rails
end
['APP_PATH', 'ENGINE_PATH'].each do |keyword|
test "is in a Rails application if #{exe} exists and contains #{keyword}" do
write exe, keyword
+ loader.exec_app_rails
+
expects_exec exe
- Rails::AppRailsLoader.exec_app_rails
end
test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do
write exe
- assert !Rails::AppRailsLoader.exec_app_rails
+ assert !loader.exec_app_rails
end
test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do
@@ -54,8 +68,9 @@ class AppRailsLoaderTest < ActiveSupport::TestCase
Dir.chdir('foo/bar')
+ loader.exec_app_rails
+
expects_exec exe
- Rails::AppRailsLoader.exec_app_rails
# Compare the realpath in case either of them has symlinks.
#
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index b235b51d90..8f091cfdbf 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -50,6 +50,8 @@ module ApplicationTests
end
RUBY
+ add_to_env_config "development", "config.assets.digest = false"
+
require "#{app_path}/config/environment"
get "/assets/demo.js"
@@ -189,7 +191,6 @@ module ApplicationTests
end
test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do
- add_to_config "config.assets.digest = true"
add_to_config "config.action_controller.perform_caching = false"
ENV["RAILS_ENV"] = "production"
@@ -199,10 +200,9 @@ 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
- add_to_config "config.assets.digest = true"
precompile!
manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first
@@ -214,8 +214,6 @@ module ApplicationTests
test "the manifest file should be saved by default in the same assets folder" do
app_file "app/assets/javascripts/application.js", "alert();"
- # digest is default in false, we must enable it for test environment
- add_to_config "config.assets.digest = true"
add_to_config "config.assets.prefix = '/x'"
precompile!
@@ -248,7 +246,6 @@ module ApplicationTests
test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do
app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
- # digest is default in false, we must enable it for test environment
add_to_env_config "test", "config.assets.digest = true"
precompile!('RAILS_ENV=test')
@@ -260,7 +257,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!
@@ -280,12 +277,9 @@ module ApplicationTests
test "precompile appends the md5 hash to files referenced with asset_path and run in production with digest true" do
app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
- add_to_config "config.assets.compile = true"
- add_to_config "config.assets.digest = true"
-
- ENV["RAILS_ENV"] = nil
- precompile!('RAILS_GROUPS=assets')
+ ENV["RAILS_ENV"] = "production"
+ precompile!
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -341,6 +335,8 @@ module ApplicationTests
end
RUBY
+ add_to_env_config "development", "config.assets.digest = false"
+
require "#{app_path}/config/environment"
class ::OmgController < ActionController::Base
@@ -365,6 +361,8 @@ module ApplicationTests
app_file "app/assets/javascripts/demo.js", "alert();"
+ add_to_env_config "development", "config.assets.digest = false"
+
require "#{app_path}/config/environment"
get "/assets/demo.js"
@@ -394,7 +392,6 @@ module ApplicationTests
app_file "app/assets/javascripts/application.js", "//= require_tree ."
app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>"
- add_to_config "config.assets.digest = false"
precompile!
assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first)
end
@@ -414,7 +411,6 @@ module ApplicationTests
test "digested assets are not mistakenly removed" do
app_file "app/assets/application.js", "alert();"
add_to_config "config.assets.compile = true"
- add_to_config "config.assets.digest = true"
precompile!
@@ -437,6 +433,7 @@ module ApplicationTests
test "asset urls should use the request's protocol by default" do
app_with_assets_in_view
add_to_config "config.asset_host = 'example.com'"
+ add_to_env_config "development", "config.assets.digest = false"
require "#{app_path}/config/environment"
class ::PostsController < ActionController::Base; end
@@ -448,23 +445,24 @@ 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'"
+ add_to_env_config "development", "config.assets.digest = false"
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}"
+ add_to_env_config "development", "config.assets.digest = false"
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/base_test.rb b/railties/test/application/configuration/base_test.rb
new file mode 100644
index 0000000000..d6a82b139d
--- /dev/null
+++ b/railties/test/application/configuration/base_test.rb
@@ -0,0 +1,37 @@
+require 'isolation/abstract_unit'
+require 'rack/test'
+require 'env_helpers'
+
+module ApplicationTests
+ module ConfigurationTests
+ class BaseTest < ActiveSupport::TestCase
+ def setup
+ build_app
+ boot_rails
+ FileUtils.rm_rf("#{app_path}/config/environments")
+ end
+
+ def teardown
+ teardown_app
+ FileUtils.rm_rf(new_app) if File.directory?(new_app)
+ end
+
+ private
+ def new_app
+ File.expand_path("#{app_path}/../new_app")
+ end
+
+ def copy_app
+ FileUtils.cp_r(app_path, new_app)
+ end
+
+ def app
+ @app ||= Rails.application
+ end
+
+ def require_environment
+ require "#{app_path}/config/environment"
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb
new file mode 100644
index 0000000000..045537fc28
--- /dev/null
+++ b/railties/test/application/configuration/custom_test.rb
@@ -0,0 +1,15 @@
+require 'application/configuration/base_test'
+
+class ApplicationTests::ConfigurationTests::CustomTest < ApplicationTests::ConfigurationTests::BaseTest
+ test 'access custom configuration point' do
+ add_to_config <<-RUBY
+ config.x.resque.inline_jobs = :always
+ config.x.resque.timeout = 60
+ RUBY
+ require_environment
+
+ assert_equal :always, Rails.configuration.x.resque.inline_jobs
+ assert_equal 60, Rails.configuration.x.resque.timeout
+ assert_nil Rails.configuration.x.resque.nothing
+ end
+end
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index b2d0e7e202..e661b6f4cc 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -8,6 +8,12 @@ end
class ::MyOtherMailInterceptor < ::MyMailInterceptor; end
+class ::MyPreviewMailInterceptor
+ def self.previewing_email(email); email; end
+end
+
+class ::MyOtherPreviewMailInterceptor < ::MyPreviewMailInterceptor; end
+
class ::MyMailObserver
def self.delivered_email(email); email; end
end
@@ -60,13 +66,25 @@ module ApplicationTests
config.action_dispatch.show_exceptions = true
RUBY
+ app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY
+ class CreateUser < ActiveRecord::Migration
+ def change
+ create_table :users
+ end
+ end
+ RUBY
+
require "#{app_path}/config/environment"
- ActiveRecord::Migrator.stubs(:needs_migration?).returns(true)
- ActiveRecord::NullMigration.any_instance.stubs(:mtime).returns(1)
- get "/foo"
- assert_equal 500, last_response.status
- assert_match "ActiveRecord::PendingMigrationError", last_response.body
+ ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"]
+
+ begin
+ get "/foo"
+ assert_equal 500, last_response.status
+ assert_match "ActiveRecord::PendingMigrationError", last_response.body
+ ensure
+ ActiveRecord::Migrator.migrations_paths = nil
+ end
end
test "Rails.groups returns available groups" do
@@ -360,12 +378,14 @@ module ApplicationTests
test "default method for update can be changed" do
app_file 'app/models/post.rb', <<-RUBY
class Post
- extend ActiveModel::Naming
+ include ActiveModel::Model
def to_key; [1]; end
def persisted?; true; end
end
RUBY
+ token = "cf50faa3fe97702ca1ae"
+
app_file 'app/controllers/posts_controller.rb', <<-RUBY
class PostsController < ApplicationController
def show
@@ -375,6 +395,10 @@ module ApplicationTests
def update
render text: "update"
end
+
+ private
+
+ def form_authenticity_token; token; end # stub the authenticy token
end
RUBY
@@ -386,8 +410,6 @@ module ApplicationTests
require "#{app_path}/config/environment"
- token = "cf50faa3fe97702ca1ae"
- PostsController.any_instance.stubs(:form_authenticity_token).returns(token)
params = {authenticity_token: token}
get "/posts/1"
@@ -418,7 +440,7 @@ module ApplicationTests
end
get "/"
- assert last_response.body =~ /_xsrf_token_here/
+ assert_match "_xsrf_token_here", last_response.body
end
test "sets ActionDispatch.test_app" do
@@ -460,6 +482,32 @@ module ApplicationTests
assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors")
end
+ test "registers preview interceptors with ActionMailer" do
+ add_to_config <<-RUBY
+ config.action_mailer.preview_interceptors = MyPreviewMailInterceptor
+ RUBY
+
+ require "#{app_path}/config/environment"
+ require "mail"
+
+ _ = ActionMailer::Base
+
+ assert_equal [::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
+ end
+
+ test "registers multiple preview interceptors with ActionMailer" do
+ add_to_config <<-RUBY
+ config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"]
+ RUBY
+
+ require "#{app_path}/config/environment"
+ require "mail"
+
+ _ = ActionMailer::Base
+
+ assert_equal [MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
+ end
+
test "registers observers with ActionMailer" do
add_to_config <<-RUBY
config.action_mailer.observers = MyMailObserver
@@ -489,7 +537,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"
@@ -500,7 +548,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
@@ -511,7 +559,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"
@@ -522,7 +570,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
@@ -682,6 +730,44 @@ module ApplicationTests
assert_match "We're sorry, but something went wrong", last_response.body
end
+ test "config.action_controller.always_permitted_parameters are: controller, action by default" do
+ require "#{app_path}/config/environment"
+ assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters
+ end
+
+ test "config.action_controller.always_permitted_parameters = ['controller', 'action', 'format']" do
+ add_to_config <<-RUBY
+ config.action_controller.always_permitted_parameters = %w( controller action format )
+ RUBY
+ require "#{app_path}/config/environment"
+ assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters
+ end
+
+ test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exeception" do
+ app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ class PostsController < ActionController::Base
+ def create
+ render text: params.permit(post: [:title])
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ config.action_controller.always_permitted_parameters = %w( controller action format )
+ config.action_controller.action_on_unpermitted_parameters = :raise
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
+
+ post "/posts", {post: {"title" =>"zomg"}, format: "json"}
+ assert_equal 200, last_response.status
+ end
+
test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do
ENV["RAILS_ENV"] = "development"
@@ -762,7 +848,7 @@ module ApplicationTests
end
end
- test "lookup config.log_level with custom logger (stdlib Logger)" do
+ test "config.log_level with custom logger" do
make_basic_app do |app|
app.config.logger = Logger.new(STDOUT)
app.config.log_level = :info
@@ -770,19 +856,6 @@ module ApplicationTests
assert_equal Logger::INFO, Rails.logger.level
end
- test "assign log_level as is with custom logger (third party logger)" do
- logger_class = Class.new do
- attr_accessor :level
- end
- logger_instance = logger_class.new
- make_basic_app do |app|
- app.config.logger = logger_instance
- app.config.log_level = :info
- end
- assert_equal logger_instance, Rails.logger
- assert_equal :info, Rails.logger.level
- end
-
test "respond_to? accepts include_private" do
make_basic_app
@@ -806,5 +879,213 @@ module ApplicationTests
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
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ rake_tasks do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ require 'rake'
+ require 'rake/testtask'
+ require 'rdoc/task'
+
+ Rails.application.load_tasks
+ assert Rails.configuration.ran_block
+ end
+
+ test "generators block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ generators do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ Rails.application.load_generators
+ assert Rails.configuration.ran_block
+ end
+
+ test "console block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ console do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ Rails.application.load_console
+ assert Rails.configuration.ran_block
+ end
+
+ test "runner block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ runner do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ Rails.application.load_runner
+ assert Rails.configuration.ran_block
+ end
+
+ test "loading the first existing database configuration available" do
+ app_file 'config/environments/development.rb', <<-RUBY
+
+ Rails.application.configure do
+ config.paths.add 'config/database', with: 'config/nonexistant.yml'
+ config.paths['config/database'] << 'config/database.yml'
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_kind_of Hash, Rails.application.config.database_configuration
+ end
+
+ test 'config.action_mailer.show_previews defaults to true in development' do
+ Rails.env = "development"
+ require "#{app_path}/config/environment"
+
+ assert Rails.application.config.action_mailer.show_previews
+ end
+
+ test 'config.action_mailer.show_previews defaults to false in production' do
+ Rails.env = "production"
+ require "#{app_path}/config/environment"
+
+ assert_equal false, Rails.application.config.action_mailer.show_previews
+ end
+
+ test 'config.action_mailer.show_previews can be set in the configuration file' do
+ Rails.env = "production"
+ add_to_config <<-RUBY
+ config.action_mailer.show_previews = true
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal true, Rails.application.config.action_mailer.show_previews
+ end
+
+ test "config_for loads custom configuration from yaml files" do
+ app_file 'config/custom.yml', <<-RUBY
+ development:
+ key: 'custom key'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ end
+
+ test "config_for raises an exception if the file does not exist" do
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ exception = assert_raises(RuntimeError) do
+ require "#{app_path}/config/environment"
+ end
+
+ assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message
+ end
+
+ test "config_for without the environment configured returns an empty hash" do
+ app_file 'config/custom.yml', <<-RUBY
+ test:
+ key: 'custom key'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal({}, Rails.application.config.my_custom_config)
+ end
+
+ test "config_for with empty file returns an empty hash" do
+ app_file 'config/custom.yml', <<-RUBY
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal({}, Rails.application.config.my_custom_config)
+ end
+
+ test "config_for containing ERB tags should evaluate" do
+ app_file 'config/custom.yml', <<-RUBY
+ development:
+ key: <%= 'custom key' %>
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ end
+
+ test "config_for with syntax error show a more descritive exception" do
+ app_file 'config/custom.yml', <<-RUBY
+ development:
+ key: foo:
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ exception = assert_raises(RuntimeError) do
+ require "#{app_path}/config/environment"
+ end
+
+ assert_match 'YAML syntax error occurred while parsing', exception.message
+ end
end
end
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index 3601a58f67..ae550331bd 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -50,7 +50,7 @@ module ApplicationTests
assert_equal "test.rails", ActionMailer::Base.default_url_options[:host]
end
- test "does not include url helpers as action methods" do
+ test "includes url helpers as action methods" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo
@@ -66,8 +66,8 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert Foo.method_defined?(:foo_path)
+ assert Foo.method_defined?(:foo_url)
assert Foo.method_defined?(:main_app)
- assert_equal Set.new(["notify"]), Foo.action_methods
end
test "allows to not load all helpers for controllers" do
@@ -216,8 +216,8 @@ module ApplicationTests
require "#{app_path}/config/environment"
orig_database_url = ENV.delete("DATABASE_URL")
orig_rails_env, Rails.env = Rails.env, 'development'
- database_url_db_name = File.join(app_path, "db/database_url_db.sqlite3")
- ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}"
+ database_url_db_name = "db/database_url_db.sqlite3"
+ 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 bc34897cdf..9ee54796a4 100644
--- a/railties/test/application/initializers/i18n_test.rb
+++ b/railties/test/application/initializers/i18n_test.rb
@@ -184,28 +184,13 @@ en:
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
+ load_app
assert_equal false, I18n.enforce_available_locales
assert_nothing_raised do
@@ -220,8 +205,7 @@ en:
config.i18n.default_locale = :fr
RUBY
- output = capture(:stderr) { load_app }
- assert_no_match %r{deprecated.*enforce_available_locales}, output
+ load_app
assert_equal false, I18n.enforce_available_locales
assert_nothing_raised do
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index c588fd7012..55e917c3ec 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -26,6 +26,20 @@ module ApplicationTests
assert_equal 404, last_response.status
end
+ test "/rails/mailers is accessible with correct configuraiton" do
+ add_to_config "config.action_mailer.show_previews = true"
+ app("production")
+ get "/rails/mailers"
+ assert_equal 200, last_response.status
+ end
+
+ test "/rails/mailers is not accessible with show_previews = false" do
+ add_to_config "config.action_mailer.show_previews = false"
+ app("development")
+ 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
@@ -403,6 +417,58 @@ module ApplicationTests
assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body
end
+ test "*_path helpers emit a deprecation" do
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index'
+ end
+ RUBY
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def path_in_view
+ mail to: "to@example.org"
+ end
+
+ def path_in_mailer
+ @url = foo_path
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template 'notifier/path_in_view', "<%= link_to 'foo', foo_path %>"
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def path_in_view
+ Notifier.path_in_view
+ end
+
+ def path_in_mailer
+ Notifier.path_in_mailer
+ end
+ end
+ RUBY
+
+ app('development')
+
+ assert_deprecated do
+ get "/rails/mailers/notifier/path_in_view.html"
+ assert_equal 200, last_response.status
+ end
+
+ html_template 'notifier/path_in_mailer', "No ERB in here"
+
+ assert_deprecated do
+ get "/rails/mailers/notifier/path_in_mailer.html"
+ assert_equal 200, last_response.status
+ end
+ end
+
private
def build_app
super
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 1557b90d27..33eb034b1c 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -188,7 +188,7 @@ module ApplicationTests
end
end
- etag = "5af83e3196bf99f440f31f2e1a6c9afe".inspect
+ etag = "W/" + "5af83e3196bf99f440f31f2e1a6c9afe".inspect
get "/"
assert_equal 200, last_response.status
diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
index 5bfea599e0..9ebf163671 100644
--- a/railties/test/application/multiple_applications_test.rb
+++ b/railties/test/application/multiple_applications_test.rb
@@ -21,6 +21,12 @@ module ApplicationTests
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
@@ -30,23 +36,23 @@ module ApplicationTests
end
def test_initialization_of_application_with_previous_config
- application1 = AppTemplate::Application.new(config: Rails.application.config)
- application2 = AppTemplate::Application.new
+ application1 = AppTemplate::Application.create(config: Rails.application.config)
+ application2 = AppTemplate::Application.create
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
+ application1 = AppTemplate::Application.create(railties: Rails.application.railties)
+ application2 = AppTemplate::Application.create
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(
+ application1 = AppTemplate::Application.create(
config: Rails.application.config,
railties: Rails.application.railties,
routes_reloader: Rails.application.routes_reloader,
@@ -66,26 +72,26 @@ module ApplicationTests
end
def test_rake_tasks_defined_on_different_applications_go_to_the_same_class
- $run_count = 0
+ run_count = 0
application1 = AppTemplate::Application.new
application1.rake_tasks do
- $run_count += 1
+ run_count += 1
end
application2 = AppTemplate::Application.new
application2.rake_tasks do
- $run_count += 1
+ 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"
+ 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"
+ 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
@@ -94,36 +100,56 @@ module ApplicationTests
def test_initializers_run_on_different_applications_go_to_the_same_class
application1 = AppTemplate::Application.new
- $run_count = 0
+ run_count = 0
AppTemplate::Application.initializer :init0 do
- $run_count += 1
+ run_count += 1
end
application1.initializer :init1 do
- $run_count += 1
+ run_count += 1
end
AppTemplate::Application.new.initializer :init2 do
- $run_count += 1
+ run_count += 1
end
- assert_equal 0, $run_count, "Without loading the initializers, the count should be 0"
+ 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"
+ 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 }
+ 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"
+ 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"
+ assert_equal 2, run_count, "There should have been two runners that increment the count"
end
def test_isolate_namespace_on_an_application
diff --git a/railties/test/application/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb
index 701843a6fd..0082ec9cd2 100644
--- a/railties/test/application/rack/logger_test.rb
+++ b/railties/test/application/rack/logger_test.rb
@@ -11,10 +11,12 @@ module ApplicationTests
def setup
build_app
+ add_to_config <<-RUBY
+ config.logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ RUBY
+
require "#{app_path}/config/environment"
super
- @logger = MockLogger.new
- Rails.stubs(:logger).returns(@logger)
end
def teardown
@@ -23,7 +25,7 @@ module ApplicationTests
end
def logs
- @logs ||= @logger.logged(:info).join("\n")
+ @logs ||= Rails.logger.logged(:info).join("\n")
end
test "logger logs proper HTTP GET verb and path" do
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 35d9c31c1e..15414db00f 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -17,66 +17,57 @@ module ApplicationTests
end
def database_url_db_name
- File.join(app_path, "db/database_url_db.sqlite3")
+ "db/database_url_db.sqlite3"
end
def set_database_url
- ENV['DATABASE_URL'] = File.join("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.exist?(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.exist?(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(%r{database:\s+\S*#{Regexp.escape(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
@@ -97,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
@@ -110,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 environment db:drop db:structure: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"
#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
@@ -157,9 +154,9 @@ module ApplicationTests
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
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index b7fd5d02c5..a3819b93b2 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -58,7 +58,7 @@ module ApplicationTests
end
test 'migration status when schema migrations table is not present' do
- output = Dir.chdir(app_path){ `rake db:migrate:status` }
+ output = Dir.chdir(app_path){ `rake db:migrate:status 2>&1` }
assert_equal "Schema migrations table does not exist yet.\n", output
end
@@ -184,6 +184,21 @@ module ApplicationTests
assert_match(/create_table "books"/, structure_dump)
end
end
+
+ test 'test migration status migrated file is deleted' do
+ Dir.chdir(app_path) do
+ `rails generate model user username:string password:string;
+ rails generate migration add_email_to_users email:string;
+ rake db:migrate
+ rm db/migrate/*email*.rb`
+
+ output = `rake db:migrate:status`
+ File.write('test.txt', output)
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index 05f6338b68..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,48 +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/assets/stylesheets/application.css.sass", "// TODO: note in sass"
- app_file "app/assets/stylesheets/application.css.less", "// TODO: note in less"
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 sass/, output)
- assert_match(/note in less/, 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 12, lines.size
-
- lines.each do |line|
- assert_equal 4, line[0].size
- assert_equal ' ', line[1]
- end
+ assert_equal 9, lines.size
+ assert_equal [4], lines.map(&:size).uniq
end
end
@@ -72,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)
@@ -92,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
@@ -108,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)
@@ -129,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
@@ -150,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 317e73245c..e8c8de9f73 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -271,5 +271,16 @@ module ApplicationTests
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/test_test.rb b/railties/test/application/test_test.rb
index a223180169..c724c867ec 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -67,7 +67,7 @@ module ApplicationTests
assert_match %r{/app/test/unit/failing_test\.rb}, output
end
- test "migrations" do
+ test "ruby schema migrations" do
output = script('generate model user name:string')
version = output.match(/(\d+)_create_users\.rb/)[1]
@@ -104,6 +104,95 @@ module ApplicationTests
assert !result.include?("create_table(:users)")
end
+ test "sql structure 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/structure.sql', ''
+ app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
+
+ app_file 'db/structure.sql', <<-SQL
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
+ INSERT INTO schema_migrations (version) VALUES ('#{version}');
+ SQL
+
+ 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"
+
+ assert_successful_test_run('models/user_test.rb')
+ end
+
+ test "sql structure migrations when adding column to existing table" do
+ output_1 = script('generate model user name:string')
+ version_1 = output_1.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 'config/initializers/enable_sql_schema_format.rb', <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ app_file 'db/structure.sql', <<-SQL
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
+ INSERT INTO schema_migrations (version) VALUES ('#{version_1}');
+ SQL
+
+ assert_successful_test_run('models/user_test.rb')
+
+ output_2 = script('generate migration add_email_to_users')
+ version_2 = output_2.match(/(\d+)_add_email_to_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", email: "jon@doe.com"
+ end
+ end
+ RUBY
+
+ app_file 'db/structure.sql', <<-SQL
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "email" varchar(255));
+ INSERT INTO schema_migrations (version) VALUES ('#{version_1}');
+ INSERT INTO schema_migrations (version) VALUES ('#{version_2}');
+ SQL
+
+ assert_successful_test_run('models/user_test.rb')
+ end
+
private
def assert_unsuccessful_run(name, message)
result = run_test_file(name)
diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb
index a34beaedb3..4aea3e980f 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -6,7 +6,13 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
include EnvHelpers
class FakeConsole
- def self.start; end
+ def self.started?
+ @started
+ end
+
+ def self.start
+ @started = true
+ end
end
def test_sandbox_option
@@ -19,37 +25,47 @@ 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
def test_start
- FakeConsole.expects(:start)
start
+
+ assert app.console.started?
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)
+ def test_start_with_sandbox
+ start ["--sandbox"]
+
- silence_stream(STDOUT) { rails_console.start }
+ assert app.console.started?
+ assert app.sandbox
+ assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
end
- def test_start_with_sandbox
- app.expects(:sandbox=).with(true)
- FakeConsole.expects(:start)
+ if RUBY_VERSION < '2.0.0'
+ def test_debugger_option
+ console = Rails::Console.new(app, parse_arguments(["--debugger"]))
+ assert console.debugger?
+ end
- start ["--sandbox"]
+ def test_no_options_does_not_set_debugger_flag
+ console = Rails::Console.new(app, parse_arguments([]))
+ assert !console.debugger?
+ end
- assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
+ def test_start_with_debugger
+ stubbed_console = Class.new(Rails::Console) do
+ def require_debugger
+ end
+ end
+
+ rails_console = stubbed_console.new(app, parse_arguments(["--debugger"]))
+ silence_stream(STDOUT) { rails_console.start }
+ end
end
def test_console_with_environment
@@ -58,7 +74,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def test_console_defaults_to_IRB
- app = build_app(console: nil)
+ app = build_app(nil)
assert_equal IRB, Rails::Console.new(app).console
end
@@ -109,8 +125,12 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
- Rails::Console.stubs(:available_environments).returns(['dev'])
- options = Rails::Console.parse_arguments(['dev'])
+ stubbed_console = Class.new(Rails::Console) do
+ def available_environments
+ ['dev']
+ end
+ end
+ options = stubbed_console.parse_arguments(['dev'])
assert_match('dev', options[:environment])
end
@@ -125,15 +145,29 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def app
- @app ||= build_app(console: FakeConsole)
+ @app ||= build_app(FakeConsole)
end
- def build_app(config)
- config = mock("config", config)
- app = mock("app", config: config)
- app.stubs(:sandbox=).returns(nil)
- app.expects(:load_console)
- app
+ def build_app(console)
+ mocked_console = Class.new do
+ attr_reader :sandbox, :console
+
+ def initialize(console)
+ @console = console
+ end
+
+ def config
+ self
+ end
+
+ def sandbox=(arg)
+ @sandbox = arg
+ end
+
+ def load_console
+ end
+ end
+ mocked_console.new(console)
end
def parse_arguments(args)
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index 24db395e6e..ede08e7b86 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'minitest/mock'
require 'rails/commands/dbconsole'
class Rails::DBConsoleTest < ActiveSupport::TestCase
@@ -26,20 +27,21 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
"timeout"=> "3000"
}
}
- app_db_config(config_sample)
- assert_equal Rails::DBConsole.new.config, config_sample["test"]
+ app_db_config(config_sample) do
+ assert_equal Rails::DBConsole.new.config, config_sample["test"]
+ end
end
def test_config_with_no_db_config
- app_db_config(nil)
- assert_raise(ActiveRecord::AdapterNotSpecified) {
- Rails::DBConsole.new.config
- }
+ app_db_config(nil) do
+ assert_raise(ActiveRecord::AdapterNotSpecified) {
+ Rails::DBConsole.new.config
+ }
+ end
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",
@@ -50,7 +52,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
"pool" => "5",
"timeout" => "3000"
}.sort
- assert_equal expected, Rails::DBConsole.new.config.sort
+
+ app_db_config(nil) do
+ assert_equal expected, Rails::DBConsole.new.config.sort
+ end
end
def test_config_choose_database_url_if_exists
@@ -68,8 +73,9 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
"timeout" => "3000"
}
}
- app_db_config(sample_config)
- assert_equal host, Rails::DBConsole.new.config["host"]
+ app_db_config(sample_config) do
+ assert_equal host, Rails::DBConsole.new.config["host"]
+ end
end
def test_env
@@ -78,58 +84,65 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
ENV['RAILS_ENV'] = nil
ENV['RACK_ENV'] = nil
- Rails.stubs(:respond_to?).with(:env).returns(false)
- assert_equal Rails::DBConsole.new.environment, "development"
+ Rails.stub(:respond_to?, false) do
+ assert_equal Rails::DBConsole.new.environment, "development"
- ENV['RACK_ENV'] = "rack_env"
- assert_equal Rails::DBConsole.new.environment, "rack_env"
+ ENV['RACK_ENV'] = "rack_env"
+ assert_equal Rails::DBConsole.new.environment, "rack_env"
- ENV['RAILS_ENV'] = "rails_env"
- assert_equal Rails::DBConsole.new.environment, "rails_env"
+ ENV['RAILS_ENV'] = "rails_env"
+ assert_equal Rails::DBConsole.new.environment, "rails_env"
+ end
ensure
ENV['RAILS_ENV'] = "test"
end
def test_rails_env_is_development_when_argument_is_dev
- Rails::DBConsole.stubs(:available_environments).returns(['development', 'test'])
- options = Rails::DBConsole.new.send(:parse_arguments, ['dev'])
- assert_match('development', options[:environment])
+ dbconsole = Rails::DBConsole.new
+
+ dbconsole.stub(:available_environments, ['development', 'test']) do
+ options = dbconsole.send(:parse_arguments, ['dev'])
+ assert_match('development', options[:environment])
+ end
end
def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
- Rails::DBConsole.stubs(:available_environments).returns(['dev'])
- options = Rails::DBConsole.new.send(:parse_arguments, ['dev'])
- assert_match('dev', options[:environment])
+ dbconsole = Rails::DBConsole.new
+
+ dbconsole.stub(:available_environments, ['dev']) do
+ options = dbconsole.send(:parse_arguments, ['dev'])
+ assert_match('dev', options[:environment])
+ end
end
def test_mysql
- dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db')
start(adapter: 'mysql', database: 'db')
assert !aborted
+ assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args
end
def test_mysql_full
- dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db')
start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8')
assert !aborted
+ assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_mysql_include_password
- dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--user=user', '--password=qwerty', 'db')
start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p'])
assert !aborted
+ assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_postgresql
- dbconsole.expects(:find_cmd_and_exec).with('psql', 'db')
start(adapter: 'postgresql', database: 'db')
assert !aborted
+ assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_postgresql_full
- dbconsole.expects(:find_cmd_and_exec).with('psql', 'db')
start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432)
assert !aborted
+ assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
assert_equal 'user', ENV['PGUSER']
assert_equal 'host', ENV['PGHOST']
assert_equal '5432', ENV['PGPORT']
@@ -137,60 +150,60 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
def test_postgresql_include_password
- dbconsole.expects(:find_cmd_and_exec).with('psql', 'db')
start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p'])
assert !aborted
+ assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
assert_equal 'user', ENV['PGUSER']
assert_equal 'q1w2e3', ENV['PGPASSWORD']
end
def test_sqlite
- dbconsole.expects(:find_cmd_and_exec).with('sqlite', 'db')
start(adapter: 'sqlite', database: 'db')
assert !aborted
+ assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('db.sqlite3').to_s)
start(adapter: 'sqlite3', database: 'db.sqlite3')
assert !aborted
+ assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_mode
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-html', Rails.root.join('db.sqlite3').to_s)
start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html'])
assert !aborted
+ assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_header
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-header', Rails.root.join('db.sqlite3').to_s)
start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header'])
+ assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_absolute_path
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '/tmp/db.sqlite3')
start(adapter: 'sqlite3', database: '/tmp/db.sqlite3')
assert !aborted
+ assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_without_defined_rails_root
- Rails.stubs(:respond_to?)
- Rails.expects(:respond_to?).with(:root).once.returns(false)
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('../config/db.sqlite3').to_s)
- start(adapter: 'sqlite3', database: 'config/db.sqlite3')
- assert !aborted
+ Rails.stub(:respond_to?, false) do
+ start(adapter: 'sqlite3', database: 'config/db.sqlite3')
+ assert !aborted
+ assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
+ end
end
def test_oracle
- dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db')
start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret')
assert !aborted
+ assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args
end
def test_oracle_include_password
- dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user/secret@db')
start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p'])
assert !aborted
+ assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args
end
def test_unknown_command_line_client
@@ -223,16 +236,27 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
private
def app_db_config(results)
- Rails.application.config.stubs(:database_configuration).returns(results || {})
+ Rails.application.config.stub(:database_configuration, results || {}) do
+ yield
+ end
end
def dbconsole
- @dbconsole ||= Rails::DBConsole.new(nil)
+ @dbconsole ||= Class.new(Rails::DBConsole) do
+ attr_reader :find_cmd_and_exec_args
+
+ def find_cmd_and_exec(*args)
+ @find_cmd_and_exec_args = args
+ end
+ end.new(nil)
end
def start(config = {}, argv = [])
- dbconsole.stubs(config: config.stringify_keys, arguments: argv)
- capture_abort { dbconsole.start }
+ dbconsole.stub(:config, config.stringify_keys) do
+ dbconsole.stub(:arguments, argv) do
+ capture_abort { dbconsole.start }
+ end
+ end
end
def capture_abort
diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb
index 7970913d21..f46fb748f5 100644
--- a/railties/test/engine_test.rb
+++ b/railties/test/engine_test.rb
@@ -11,4 +11,15 @@ class EngineTest < ActiveSupport::TestCase
assert !engine.routes?
end
+
+ def test_application_can_be_subclassed
+ klass = Class.new(Rails::Application) do
+ attr_reader :hello
+ def initialize
+ @hello = "world"
+ super
+ end
+ end
+ assert_equal "world", klass.instance.hello
+ end
end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 0db40c1d32..a4337926d1 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -1,6 +1,7 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/app/app_generator'
require 'env_helpers'
+require 'mocha/setup' # FIXME: stop using mocha
class ActionsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -41,13 +42,13 @@ class ActionsTest < Rails::Generators::TestCase
def test_add_source_adds_source_to_gemfile
run_generator
action :add_source, 'http://gems.github.com'
- assert_file 'Gemfile', /source "http:\/\/gems\.github\.com"/
+ assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/
end
def test_gem_should_put_gem_dependency_in_gemfile
run_generator
action :gem, 'will-paginate'
- assert_file 'Gemfile', /gem "will\-paginate"/
+ assert_file 'Gemfile', /gem 'will\-paginate'/
end
def test_gem_with_version_should_include_version_in_gemfile
@@ -55,7 +56,7 @@ class ActionsTest < Rails::Generators::TestCase
action :gem, 'rspec', '>=2.0.0.a5'
- assert_file 'Gemfile', /gem "rspec", ">=2.0.0.a5"/
+ assert_file 'Gemfile', /gem 'rspec', '>=2.0.0.a5'/
end
def test_gem_should_insert_on_separate_lines
@@ -66,8 +67,8 @@ class ActionsTest < Rails::Generators::TestCase
action :gem, 'rspec'
action :gem, 'rspec-rails'
- assert_file 'Gemfile', /^gem "rspec"$/
- assert_file 'Gemfile', /^gem "rspec-rails"$/
+ assert_file 'Gemfile', /^gem 'rspec'$/
+ assert_file 'Gemfile', /^gem 'rspec-rails'$/
end
def test_gem_should_include_options
@@ -75,7 +76,15 @@ class ActionsTest < Rails::Generators::TestCase
action :gem, 'rspec', github: 'dchelimsky/rspec', tag: '1.2.9.rc1'
- assert_file 'Gemfile', /gem "rspec", github: "dchelimsky\/rspec", tag: "1\.2\.9\.rc1"/
+ assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/
+ end
+
+ def test_gem_falls_back_to_inspect_if_string_contains_single_quote
+ run_generator
+
+ action :gem, 'rspec', ">=2.0'0"
+
+ assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/
end
def test_gem_group_should_wrap_gems_in_a_group
@@ -89,7 +98,7 @@ class ActionsTest < Rails::Generators::TestCase
gem 'fakeweb'
end
- assert_file 'Gemfile', /\ngroup :development, :test do\n gem "rspec-rails"\nend\n\ngroup :test do\n gem "fakeweb"\nend/
+ assert_file 'Gemfile', /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/
end
def test_environment_should_include_data_in_environment_initializer_block
@@ -234,7 +243,7 @@ class ActionsTest < Rails::Generators::TestCase
protected
def action(*args, &block)
- silence(:stdout){ generator.send(*args, &block) }
+ capture(:stdout){ generator.send(*args, &block) }
end
end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 5811379e35..70c439672f 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -1,6 +1,7 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/app/app_generator'
require 'generators/shared_generator_tests'
+require 'mocha/setup' # FIXME: stop using mocha
DEFAULT_APP_FILES = %w(
.gitignore
@@ -21,6 +22,7 @@ DEFAULT_APP_FILES = %w(
bin/bundle
bin/rails
bin/rake
+ bin/setup
config/environments
config/initializers
config/locales
@@ -119,7 +121,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
destination_root: app_moved_root, shell: @shell
generator.send(:app_const)
- quietly { generator.send(:create_config_files) }
+ quietly { generator.send(:update_config_files) }
assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/
end
@@ -134,10 +136,46 @@ class AppGeneratorTest < Rails::Generators::TestCase
generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
generator.send(:app_const)
- quietly { generator.send(:create_config_files) }
+ quietly { generator.send(:update_config_files) }
assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/
end
+ def test_new_application_use_json_serialzier
+ run_generator
+
+ assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ end
+
+ def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ Rails.application.config.root = app_root
+ Rails.application.class.stubs(:name).returns("Myapp")
+ Rails.application.stubs(:is_a?).returns(Rails::Application)
+
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ end
+
+ def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb")
+
+ Rails.application.config.root = app_root
+ Rails.application.class.stubs(:name).returns("Myapp")
+ Rails.application.stubs(:is_a?).returns(Rails::Application)
+
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ end
+
def test_application_names_are_not_singularized
run_generator [File.join(destination_root, "hats")]
assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/
@@ -156,20 +194,20 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_config_database_is_added_by_default
run_generator
assert_file "config/database.yml", /sqlite3/
- unless defined?(JRUBY_VERSION)
- assert_gem "sqlite3"
- else
+ if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcsqlite3-adapter"
+ else
+ assert_gem "sqlite3"
end
end
def test_config_another_database
run_generator([destination_root, "-d", "mysql"])
assert_file "config/database.yml", /mysql/
- unless defined?(JRUBY_VERSION)
- assert_gem "mysql2"
- else
+ if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcmysql-adapter"
+ else
+ assert_gem "mysql2"
end
end
@@ -181,10 +219,10 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_config_postgresql_database
run_generator([destination_root, "-d", "postgresql"])
assert_file "config/database.yml", /postgresql/
- unless defined?(JRUBY_VERSION)
- assert_gem "pg"
- else
+ if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcpostgresql-adapter"
+ else
+ assert_gem "pg"
end
end
@@ -213,9 +251,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_gem "activerecord-jdbc-adapter"
end
- def test_config_jdbc_database_when_no_option_given
- if defined?(JRUBY_VERSION)
- run_generator([destination_root])
+ if defined?(JRUBY_VERSION)
+ def test_config_jdbc_database_when_no_option_given
+ run_generator
assert_file "config/database.yml", /sqlite3/
assert_gem "activerecord-jdbcsqlite3-adapter"
end
@@ -237,6 +275,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
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)
end
@@ -252,23 +291,15 @@ 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
def test_inclusion_of_javascript_runtime
- run_generator([destination_root])
+ run_generator
if defined?(JRUBY_VERSION)
assert_gem "therubyrhino"
else
- assert_file "Gemfile", /# gem\s+["']therubyracer["']+, \s+platforms: :ruby$/
- end
- end
-
- def test_inclusion_of_plateform_dependent_gems
- run_generator([destination_root])
- if RUBY_ENGINE == 'rbx'
- assert_gem 'rubysl'
+ assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/
end
end
@@ -309,17 +340,20 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_inclusion_of_jbuilder
run_generator
- assert_file "Gemfile", /gem 'jbuilder'/
+ assert_gem 'jbuilder'
end
- def test_inclusion_of_debugger
+ def test_inclusion_of_a_debugger
run_generator
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_gem 'debugger'
else
- assert_file "Gemfile", /# gem 'debugger'/
+ assert_gem 'byebug'
end
end
@@ -364,7 +398,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_new_hash_style
- run_generator [destination_root]
+ run_generator
assert_file "config/initializers/session_store.rb" do |file|
assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file)
end
@@ -385,9 +419,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/
end
+ def test_web_console
+ run_generator
+ assert_gem 'web-console'
+ end
+
def test_spring
run_generator
- assert_file "Gemfile", /gem 'spring', \s+group: :development/
+ assert_gem 'spring'
end
def test_spring_binstubs
@@ -415,13 +454,80 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
-protected
+ def test_generator_if_skip_gems_is_given
+ run_generator [destination_root, "--skip-gems", "turbolinks", "coffee-rails"]
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/turbolinks/, content)
+ assert_no_match(/coffee-rails/, content)
+ end
+ assert_file "app/views/layouts/application.html.erb" do |content|
+ assert_no_match(/data-turbolinks-track/, content)
+ end
+ assert_file "app/assets/javascripts/application.js" do |content|
+ assert_no_match(/turbolinks/, 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
+
+ def test_psych_gem
+ run_generator
+ gem_regex = /gem 'psych',\s+'~> 2.0', \s+platforms: :rbx/
+
+ assert_file "Gemfile" do |content|
+ if defined?(Rubinius)
+ assert_match(gem_regex, content)
+ else
+ assert_no_match(gem_regex, content)
+ end
+ end
+ end
+
+ def test_after_bundle_callback
+ path = 'http://example.org/rails_template'
+ template = %{ after_bundle { run 'echo ran after_bundle' } }
+ template.instance_eval "def read; self; end" # Make the string respond to read
+
+ generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
+
+ bundler_first = sequence('bundle, binstubs, after_bundle')
+ generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first)
+ generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first)
+ generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first)
+
+ quietly { generator.invoke_all }
+ end
+
+ protected
def action(*args, &block)
- silence(:stdout) { generator.send(*args, &block) }
+ capture(:stdout) { generator.send(*args, &block) }
end
def assert_gem(gem)
- assert_file "Gemfile", /^gem\s+["']#{gem}["']$/
+ assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/
end
end
diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb
index a94350cbd7..31e07bc8da 100644
--- a/railties/test/generators/argv_scrubber_test.rb
+++ b/railties/test/generators/argv_scrubber_test.rb
@@ -16,7 +16,7 @@ module Rails
output = nil
exit_code = nil
scrubber.extend(Module.new {
- define_method(:puts) { |str| output = str }
+ define_method(:puts) { |string| output = string }
define_method(:exit) { |code| exit_code = code }
})
scrubber.prepare!
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index 4b2f8539d0..a7d56dd352 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -28,13 +28,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_invokes_helper
run_generator
assert_file "app/helpers/account_helper.rb"
- assert_file "test/helpers/account_helper_test.rb"
end
def test_does_not_invoke_helper_if_required
run_generator ["account", "--skip-helper"]
assert_no_file "app/helpers/account_helper.rb"
- assert_no_file "test/helpers/account_helper_test.rb"
end
def test_invokes_assets
@@ -70,6 +68,13 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
+ def test_skip_routes
+ run_generator ["account", "foo", "--skip-routes"]
+ assert_file "config/routes.rb" do |routes|
+ assert_no_match(/get 'account\/foo'/, routes)
+ end
+ end
+
def test_invokes_default_template_engine_even_with_no_action
run_generator ["account"]
assert_file "app/views/account"
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index 77ec2f1c0c..e7990de754 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -7,7 +7,7 @@ module Rails
class << self
remove_possible_method :root
def root
- @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures'))
+ @root ||= File.expand_path('../../fixtures', __FILE__)
end
end
end
@@ -41,4 +41,12 @@ module GeneratorsTestHelper
FileUtils.mkdir_p(destination)
FileUtils.cp routes, destination
end
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb
index 81d4fcb129..add04f21a4 100644
--- a/railties/test/generators/helper_generator_test.rb
+++ b/railties/test/generators/helper_generator_test.rb
@@ -13,26 +13,11 @@ class HelperGeneratorTest < Rails::Generators::TestCase
assert_file "app/helpers/admin_helper.rb", /module AdminHelper/
end
- def test_invokes_default_test_framework
- run_generator
- assert_file "test/helpers/admin_helper_test.rb", /class AdminHelperTest < ActionView::TestCase/
- end
-
- def test_logs_if_the_test_framework_cannot_be_found
- content = run_generator ["admin", "--test-framework=rspec"]
- assert_match(/rspec \[not found\]/, content)
- end
-
def test_check_class_collision
content = capture(:stderr){ run_generator ["object"] }
assert_match(/The name 'ObjectHelper' is either already used in your application or reserved/, content)
end
- def test_check_class_collision_on_tests
- content = capture(:stderr){ run_generator ["another_object"] }
- assert_match(/The name 'AnotherObjectHelperTest' is either already used in your application or reserved/, content)
- end
-
def test_namespaced_and_not_namespaced_helpers
run_generator ["products"]
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 ac5cfff229..4199e00b0d 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -1,5 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
+require 'mocha/setup' # FIXME: stop using mocha
# Mock out what we need from AR::Base.
module ActiveRecord
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index d677c21f15..7eeb084eab 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -47,7 +47,6 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
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/
end
def test_invokes_default_test_framework
@@ -229,7 +228,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_file "app/helpers/test_app/product_lines_helper.rb"
- assert_file "test/helpers/test_app/product_lines_helper_test.rb"
# Stylesheets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -260,7 +258,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_no_file "app/helpers/test_app/product_lines_helper.rb"
- assert_no_file "test/helpers/test_app/product_lines_helper_test.rb"
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
@@ -297,7 +294,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_file "app/helpers/test_app/admin/roles_helper.rb"
- assert_file "test/helpers/test_app/admin/roles_helper_test.rb"
# Stylesheets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -329,7 +325,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_no_file "app/helpers/test_app/admin/roles_helper.rb"
- assert_no_file "test/helpers/test_app/admin/roles_helper_test.rb"
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
@@ -366,7 +361,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_file "app/helpers/test_app/admin/user/special/roles_helper.rb"
- assert_file "test/helpers/test_app/admin/user/special/roles_helper_test.rb"
# Stylesheets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -397,7 +391,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_no_file "app/helpers/test_app/admin/user/special/roles_helper.rb"
- assert_no_file "test/helpers/test_app/admin/user/special/roles_helper_test.rb"
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 932cd75bcb..985644e8af 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -1,6 +1,7 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/plugin/plugin_generator'
require 'generators/shared_generator_tests'
+require 'mocha/setup' # FIXME: stop using mocha
DEFAULT_PLUGIN_FILES = %w(
.gitignore
@@ -64,14 +65,17 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/
end
- def test_inclusion_of_debugger
+ 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
- else
+ elsif RUBY_VERSION < '2.0.0'
assert_file "Gemfile", /# gem 'debugger'/
+ else
+ assert_file "Gemfile", /# gem 'byebug'/
end
end
@@ -91,7 +95,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_generating_adds_dummy_app_without_javascript_and_assets_deps
- run_generator [destination_root]
+ run_generator
assert_file "test/dummy/app/assets/stylesheets/application.css"
@@ -156,7 +160,7 @@ class PluginGeneratorTest < 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
@@ -185,22 +189,22 @@ class PluginGeneratorTest < 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
@@ -309,7 +313,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_no_file "bukkits.gemspec"
assert_file "Gemfile" do |contents|
assert_no_match('gemspec', contents)
- assert_match(/gem "rails", "~> #{Rails.version}"/, contents)
+ assert_match(/gem 'rails', '~> #{Rails.version}'/, contents)
assert_match_sqlite3(contents)
assert_no_match(/# gem "jquery-rails"/, contents)
end
@@ -320,7 +324,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_no_file "bukkits.gemspec"
assert_file "Gemfile" do |contents|
assert_no_match('gemspec', contents)
- assert_match(/gem "rails", "~> #{Rails.version}"/, contents)
+ assert_match(/gem 'rails', '~> #{Rails.version}'/, contents)
assert_match_sqlite3(contents)
end
end
@@ -331,7 +335,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
Object.const_set('APP_PATH', Rails.root)
FileUtils.touch gemfile_path
- run_generator [destination_root]
+ run_generator
assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/
ensure
@@ -355,6 +359,52 @@ class PluginGeneratorTest < 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
+ assert_file "bukkits.gemspec" do |contents|
+ assert_match name, contents
+ assert_match email, contents
+ end
+ end
+
+ def test_git_name_in_license_file
+ name = `git config user.name`.chomp rescue "TODO: Write your name"
+
+ run_generator
+ assert_file "MIT-LICENSE" do |contents|
+ assert_match 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 name, contents
+ end
+ assert_file "bukkits.gemspec" do |contents|
+ assert_match name, contents
+ assert_match email, contents
+ end
+ end
protected
def action(*args, &block)
@@ -366,10 +416,10 @@ protected
end
def assert_match_sqlite3(contents)
- unless defined?(JRUBY_VERSION)
- assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
+ if defined?(JRUBY_VERSION)
+ assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents)
else
- assert_match(/group :development do\n gem "activerecord-jdbcsqlite3-adapter"\nend/, contents)
+ assert_match(/group :development do\n gem 'sqlite3'\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..581d80d60e 100644
--- a/railties/test/generators/resource_generator_test.rb
+++ b/railties/test/generators/resource_generator_test.rb
@@ -36,7 +36,6 @@ class ResourceGeneratorTest < Rails::Generators::TestCase
assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionController::TestCase/
assert_file "app/helpers/accounts_helper.rb", /module AccountsHelper/
- assert_file "test/helpers/accounts_helper_test.rb", /class AccountsHelperTest < ActionView::TestCase/
end
def test_resource_controller_with_actions
@@ -63,19 +62,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 26e56a162c..ca972a3bdd 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -81,7 +81,6 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
def test_helper_are_invoked_with_a_pluralized_name
run_generator
assert_file "app/helpers/users_helper.rb", /module UsersHelper/
- assert_file "test/helpers/users_helper_test.rb", /class UsersHelperTest < ActionView::TestCase/
end
def test_views_are_generated
@@ -93,6 +92,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/views/layouts/users.html.erb"
end
+ def test_index_page_have_notice
+ run_generator
+
+ %w(index show).each do |view|
+ assert_file "app/views/users/#{view}.html.erb", /notice/
+ end
+ end
+
def test_functional_tests
run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"]
@@ -118,7 +125,6 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
def test_skip_helper_if_required
run_generator ["User", "name:string", "age:integer", "--no-helper"]
assert_no_file "app/helpers/users_helper.rb"
- assert_no_file "test/helpers/users_helper_test.rb"
end
def test_skip_layout_if_required
@@ -160,13 +166,6 @@ 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)
- end
- end
-
def test_model_name_option
run_generator ["Admin::User", "--model-name=User"]
assert_file "app/controllers/admin/users_controller.rb" do |content|
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index 524bbde2b7..637bde2a44 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -70,7 +70,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_file "app/helpers/product_lines_helper.rb"
- assert_file "test/helpers/product_lines_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -114,7 +113,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_no_file "app/helpers/product_lines_helper.rb"
- assert_no_file "test/helpers/product_lines_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css", /:visited/
@@ -182,7 +180,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_file "app/helpers/admin/roles_helper.rb"
- assert_file "test/helpers/admin/roles_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css", /:visited/
@@ -216,7 +213,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_no_file "app/helpers/admin/roles_helper.rb"
- assert_no_file "test/helpers/admin/roles_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css"
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 8e198d5fe1..b998fef42e 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -78,9 +78,12 @@ module SharedGeneratorTests
end
def test_template_raises_an_error_with_invalid_path
- content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) }
- assert_match(/The template \[.*\] could not be loaded/, content)
- assert_match(/non\/existent\/path/, content)
+ quietly do
+ content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) }
+
+ assert_match(/The template \[.*\] could not be loaded/, content)
+ assert_match(/non\/existent\/path/, content)
+ end
end
def test_template_is_executed_when_supplied
@@ -89,7 +92,7 @@ module SharedGeneratorTests
template.instance_eval "def read; self; end" # Make the string respond to read
generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
- assert_match(/It works!/, capture(:stdout) { generator.invoke_all })
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
end
def test_template_is_executed_when_supplied_an_https_path
@@ -98,7 +101,7 @@ module SharedGeneratorTests
template.instance_eval "def read; self; end" # Make the string respond to read
generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
- assert_match(/It works!/, capture(:stdout) { generator.invoke_all })
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
end
def test_dev_option
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index eac28badfe..127e059bf1 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -1,6 +1,7 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/model/model_generator'
require 'rails/generators/test_unit/model/model_generator'
+require 'mocha/setup' # FIXME: stop using mocha
class GeneratorsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -21,8 +22,16 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_invoke_when_generator_is_not_found
- output = capture(:stdout){ Rails::Generators.invoke :unknown }
- assert_equal "Could not find generator unknown.\n", output
+ name = :unknown
+ output = capture(:stdout){ Rails::Generators.invoke name }
+ assert_match "Could not find generator '#{name}'", output
+ assert_match "`rails generate --help`", output
+ end
+
+ def test_generator_suggestions
+ name = :migrationz
+ output = capture(:stdout){ Rails::Generators.invoke name }
+ assert_match "Maybe you meant 'migration'", output
end
def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments
@@ -143,6 +152,8 @@ class GeneratorsTest < Rails::Generators::TestCase
klass = Rails::Generators.find_by_namespace(:plugin, :remarkable)
assert klass
assert_equal "test_unit:plugin", klass.namespace
+ ensure
+ Rails::Generators.fallbacks.delete(:remarkable)
end
def test_fallbacks_for_generators_on_find_by_namespace_with_context
@@ -150,18 +161,26 @@ class GeneratorsTest < Rails::Generators::TestCase
klass = Rails::Generators.find_by_namespace(:remarkable, :rails, :plugin)
assert klass
assert_equal "test_unit:plugin", klass.namespace
+ ensure
+ Rails::Generators.fallbacks.delete(:remarkable)
end
def test_fallbacks_for_generators_on_invoke
Rails::Generators.fallbacks[:shoulda] = :test_unit
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke "shoulda:model", ["Account"]
+ ensure
+ Rails::Generators.fallbacks.delete(:shoulda)
end
def test_nested_fallbacks_for_generators
+ Rails::Generators.fallbacks[:shoulda] = :test_unit
Rails::Generators.fallbacks[:super_shoulda] = :shoulda
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke "super_shoulda:model", ["Account"]
+ ensure
+ Rails::Generators.fallbacks.delete(:shoulda)
+ Rails::Generators.fallbacks.delete(:super_shoulda)
end
def test_developer_options_are_overwritten_by_user_options
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 6c50911666..92d6a1729c 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -291,6 +291,33 @@ class ActiveSupport::TestCase
include TestHelpers::Paths
include TestHelpers::Rack
include TestHelpers::Generation
+
+ private
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
# Create a scope and build a fixture rails app
diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb
new file mode 100644
index 0000000000..13bf29d3c3
--- /dev/null
+++ b/railties/test/path_generation_test.rb
@@ -0,0 +1,88 @@
+# encoding: utf-8
+require 'abstract_unit'
+require 'active_support/core_ext/object/with_options'
+require 'active_support/core_ext/object/json'
+require 'rails'
+require 'rails/application'
+
+ROUTING = ActionDispatch::Routing
+
+class PathGenerationTest < ActiveSupport::TestCase
+ attr_reader :app
+
+ class TestSet < ROUTING::RouteSet
+ def initialize(block)
+ @block = block
+ super()
+ end
+
+ class Dispatcher < ROUTING::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
+ end
+
+ def send_request(uri_or_host, method, path, script_name = nil)
+ host = uri_or_host.host unless path
+ path ||= uri_or_host.path
+
+ params = {'PATH_INFO' => path,
+ 'REQUEST_METHOD' => method,
+ 'HTTP_HOST' => host }
+
+ params['SCRIPT_NAME'] = script_name if script_name
+
+ status, headers, body = app.call(params)
+ new_body = []
+ body.each { |part| new_body << part }
+ body.close if body.respond_to? :close
+ [status, headers, new_body]
+ end
+
+ def test_original_script_name
+ original_logger = Rails.logger
+ Rails.logger = Logger.new nil
+
+ app = Class.new(Rails::Application) {
+ attr_accessor :controller
+ def initialize
+ super
+ app = self
+ @routes = TestSet.new ->(c) { app.controller = c }
+ secrets.secret_key_base = "foo"
+ secrets.secret_token = "foo"
+ end
+ def app; routes; end
+ }
+
+ @app = app
+ app.routes.draw { resource :blogs }
+
+ url = URI("http://example.org/blogs")
+
+ send_request(url, 'GET', nil, '/FOO')
+ assert_equal '/FOO/blogs', app.instance.controller.blogs_path
+
+ send_request(url, 'GET', nil)
+ assert_equal '/blogs', app.instance.controller.blogs_path
+ ensure
+ Rails.logger = original_logger
+ end
+end
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index ed4559ec6f..1aeb9ec339 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'rails/paths'
+require 'mocha/setup' # FIXME: stop using mocha
class PathsTest < ActiveSupport::TestCase
def setup
diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb
index 6ebd47fff9..fcc79b57fb 100644
--- a/railties/test/rack_logger_test.rb
+++ b/railties/test/rack_logger_test.rb
@@ -39,11 +39,11 @@ module Rails
def setup
@subscriber = Subscriber.new
@notifier = ActiveSupport::Notifications.notifier
- notifier.subscribe 'request.action_dispatch', subscriber
+ @subscription = notifier.subscribe 'request.action_dispatch', subscriber
end
def teardown
- notifier.unsubscribe subscriber
+ notifier.unsubscribe @subscription
end
def test_notification
diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb
index a9b237d0a5..8d61af4972 100644
--- a/railties/test/rails_info_controller_test.rb
+++ b/railties/test/rails_info_controller_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'mocha/setup' # FIXME: stop using mocha
module ActionController
class Base
diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb
index 44a5fd1904..4bec302ff8 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -66,16 +66,6 @@ class InfoTest < ActiveSupport::TestCase
end
protected
- def svn_info=(info)
- Rails::Info.module_eval do
- class << self
- def svn_info
- info
- end
- end
- end
- end
-
def properties
Rails::Info.properties
end
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index c4b18e9ea5..ec64ce5941 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -34,6 +34,7 @@ module RailtiesTest
test "serving sprocket's assets" do
@plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();"
+ add_to_env_config "development", "config.assets.digest = false"
boot_rails
require 'rack/test'
@@ -111,6 +112,38 @@ module RailtiesTest
end
end
+ test 'respects the order of railties when installing migrations' do
+ @blog = engine "blog" do |plugin|
+ plugin.write "lib/blog.rb", <<-RUBY
+ module Blog
+ class Engine < ::Rails::Engine
+ end
+ end
+ RUBY
+ end
+
+ @plugin.write "db/migrate/1_create_users.rb", <<-RUBY
+ class CreateUsers < ActiveRecord::Migration
+ end
+ RUBY
+
+ @blog.write "db/migrate/2_create_blogs.rb", <<-RUBY
+ class CreateBlogs < ActiveRecord::Migration
+ end
+ RUBY
+
+ add_to_config("config.railties_order = [Bukkits::Engine, Blog::Engine, :all, :main_app]")
+
+ boot_rails
+
+ Dir.chdir(app_path) do
+ output = `bundle exec rake railties:install:migrations`.split("\n")
+
+ assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first)
+ assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last)
+ end
+ end
+
test "mountable engine should copy migrations within engine_path" do
@plugin.write "lib/bukkits.rb", <<-RUBY
module Bukkits
@@ -592,11 +625,15 @@ YAML
@plugin.write "app/models/bukkits/post.rb", <<-RUBY
module Bukkits
class Post
- extend ActiveModel::Naming
+ include ActiveModel::Model
def to_param
"1"
end
+
+ def persisted?
+ true
+ end
end
end
RUBY
@@ -704,8 +741,7 @@ YAML
@plugin.write "app/models/bukkits/post.rb", <<-RUBY
module Bukkits
class Post
- extend ActiveModel::Naming
- include ActiveModel::Conversion
+ include ActiveModel::Model
attr_accessor :title
def to_param
@@ -1077,6 +1113,7 @@ YAML
RUBY
add_to_config("config.railties_order = [:all, :main_app, Blog::Engine]")
+ add_to_env_config "development", "config.assets.digest = false"
boot_rails
diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb
index 0ef2ff2e2e..fb2071c7c3 100644
--- a/railties/test/railties/mounted_engine_test.rb
+++ b/railties/test/railties/mounted_engine_test.rb
@@ -88,18 +88,14 @@ module ApplicationTests
@plugin.write "app/models/blog/post.rb", <<-RUBY
module Blog
class Post
- extend ActiveModel::Naming
+ include ActiveModel::Model
def id
44
end
- def to_param
- id.to_s
- end
-
- def new_record?
- false
+ def persisted?
+ true
end
end
end
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index 4cbd4822be..5042d628cf 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
+ $before_configuration = 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/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 439a9e0c05..767feaf236 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -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
diff --git a/version.rb b/version.rb
index 923cab4e2a..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 = "beta1"
+ PRE = "alpha"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end